diff --git a/Cargo.toml b/Cargo.toml index e43f5ec..100b777 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,11 +34,7 @@ members = [ ] [[example]] -name = "counter-channel" -path = "examples/counter/channel.rs" - -[[example]] -name = "counter-global-state-ecs" +name = "counter" path = "examples/counter/global-state-ecs.rs" [[example]] @@ -50,8 +46,8 @@ name = "window-settings" path = "examples/window/settings.rs" [[example]] -name = "update-mode" -path = "examples/window/update-mode.rs" +name = "render-mode" +path = "examples/window/render-mode.rs" [[example]] name = "multiple-windows" diff --git a/examples/channel.rs b/examples/channel.rs deleted file mode 100644 index cc7a23e..0000000 --- a/examples/channel.rs +++ /dev/null @@ -1,63 +0,0 @@ -use bevy::{log::LogPlugin, prelude::*}; -use bevy_dioxus::desktop::prelude::*; -use dioxus::prelude::*; - -fn main() { - App::new() - .insert_resource(WindowDescriptor { - title: "Ping-Pong Example".to_string(), - ..Default::default() - }) - .add_plugin(DioxusPlugin::::new(Root)) - .add_plugin(LogPlugin) - .add_system(pong) - .run(); -} - -// UI -> Core -#[derive(Debug, Clone)] -enum CoreCommand { - Ping, -} - -// Core -> UI -#[derive(Debug, Clone)] -enum UiCommand { - Pong, -} - -// UI Component -#[allow(non_snake_case)] -fn Root(cx: Scope) -> Element { - let window = use_window::(&cx); - - use_future(&cx, (), |_| { - let rx = window.receiver(); - - async move { - while let Some(cmd) = rx.receive().await { - match cmd { - UiCommand::Pong => { - info!("🎨 Pong"); - } - } - } - } - }); - - cx.render(rsx! { - h1 { "Ping Pong Example" } - p { "💡 Press \"Ping\" and see console." } - button { - onclick: move |_| window.send(CoreCommand::Ping), - "Ping" - } - }) -} - -fn pong(mut events: EventReader, mut ui: EventWriter) { - for cmd in events.iter() { - info!("🧠 {:?}", cmd); - ui.send(UiCommand::Pong); - } -} diff --git a/examples/counter/channel.rs b/examples/counter/channel.rs deleted file mode 100644 index 1e40c4e..0000000 --- a/examples/counter/channel.rs +++ /dev/null @@ -1,107 +0,0 @@ -use bevy::{log::LogPlugin, prelude::*}; -use bevy_dioxus::desktop::prelude::*; -use dioxus::prelude::*; - -fn main() { - App::new() - .add_plugin(LogPlugin) - .add_plugin(DioxusPlugin::::new(Root)) - .add_startup_system(setup) - .add_system(handle_core_cmd) - .add_system(notify_counter_change) - .run(); -} - -// Bevy Components -#[derive(Component, Default)] -struct Count(u32); - -// Core <-> UI -#[derive(Clone, Debug)] -enum CoreCommand { - Increment, - Decrement, - Reset, -} - -#[derive(Clone, Debug)] -enum UiCommand { - CountChanged(u32), -} - -// Systems -fn setup(mut commands: Commands) { - info!("🧠 Spawn count"); - commands.spawn().insert(Count::default()); -} - -fn notify_counter_change(query: Query<&Count, Changed>, mut ui: EventWriter) { - for count in query.iter() { - info!("🧠 Counter Changed: {}", count.0); - ui.send(UiCommand::CountChanged(count.0)); - } -} - -fn handle_core_cmd(mut events: EventReader, mut query: Query<&mut Count>) { - for cmd in events.iter() { - let mut count = query.single_mut(); - match cmd { - CoreCommand::Increment => { - info!("🧠 Increment"); - count.0 += 1; - } - CoreCommand::Decrement => { - if count.0 > 0 { - info!("🧠 Decrement"); - count.0 -= 1; - } - } - CoreCommand::Reset => { - if count.0 != 0 { - info!("🧠 Reset"); - count.0 = 0; - } - } - } - } -} - -// UI Component -#[allow(non_snake_case)] -fn Root(cx: Scope) -> Element { - let window = use_window::(&cx); - let count = use_state(&cx, || 0); - let disabled = *count == 0; - - use_future(&cx, (), |_| { - let count = count.clone(); - let rx = window.receiver(); - - async move { - while let Some(cmd) = rx.receive().await { - match cmd { - UiCommand::CountChanged(c) => count.modify(|_| c), - } - } - } - }); - - cx.render(rsx! { - h1 { "Counter Example" } - p { "count: {count}" } - button { - onclick: move |_| window.send(CoreCommand::Decrement), - disabled: "{disabled}", - "-", - } - button { - onclick: move |_| window.send(CoreCommand::Reset), - disabled: "{disabled}", - "Reset" - } - button { - onclick: move |_| window.send(CoreCommand::Increment), - "+", - } - }) -} diff --git a/examples/counter/global-state-ecs.rs b/examples/counter/global-state-ecs.rs index 0cf42c2..dcfacd6 100644 --- a/examples/counter/global-state-ecs.rs +++ b/examples/counter/global-state-ecs.rs @@ -7,8 +7,8 @@ fn main() { .add_plugin(LogPlugin) .add_plugin(GlobalStatePlugin) .add_plugin(DioxusPlugin::::new(Root)) - .add_event::() - .add_startup_system(setup) + .add_plugin(GlobalStatePlugin) + .init_resource::() .add_system(handle_core_cmd) .add_system(update_global_state) .run(); @@ -20,16 +20,9 @@ struct GlobalState { disabled: bool, } -#[derive(Component, Clone, Debug, Default)] -struct Count(u32); - -#[derive(Component, Clone, Debug)] -struct Disabled(bool); - -impl Default for Disabled { - fn default() -> Self { - Self(true) - } +#[derive(Clone, Debug, Default)] +struct Count { + value: u32, } #[derive(Clone, Debug)] @@ -39,68 +32,41 @@ enum CoreCommand { Reset, } -struct UpdateGlobalState; - -fn setup(mut commands: Commands, mut update_global_state: EventWriter) { - info!("🧠 Spawn count"); - commands - .spawn() - .insert(Count::default()) - .insert(Disabled::default()); - - update_global_state.send(UpdateGlobalState); -} - -fn handle_core_cmd( - mut events: EventReader, - mut query: Query<(&mut Count, &mut Disabled)>, - mut update_global_state: EventWriter, -) { +fn handle_core_cmd(mut events: EventReader, mut count: ResMut) { for cmd in events.iter() { - let (mut count, mut disabled) = query.single_mut(); match cmd { CoreCommand::Increment => { info!("🧠 Increment"); - count.0 += 1; + count.value += 1; } CoreCommand::Decrement => { - if count.0 > 0 { + if count.value > 0 { info!("🧠 Decrement"); - count.0 -= 1; + count.value -= 1; } } CoreCommand::Reset => { - if count.0 != 0 { + if count.value != 0 { info!("🧠 Reset"); - count.0 = 0; + count.value = 0; } } }; - disabled.0 = count.0 == 0; - - update_global_state.send(UpdateGlobalState); } } -fn update_global_state( - mut events: EventReader, - query: Query<(&Count, &Disabled)>, - mut global_state: EventWriter, -) { - for _ in events.iter() { - let (count, disabled) = query.single(); - - global_state.send(GlobalState::Count(count.0)); - global_state.send(GlobalState::Disabled(disabled.0)); +fn update_global_state(count: Res, mut global_state: EventWriter) { + if count.is_changed() { + global_state.send(GlobalState::Count(count.value)); } } #[allow(non_snake_case)] fn Root(cx: Scope) -> Element { let count = use_read(&cx, COUNT); - let disabled = use_read(&cx, DISABLED); + let disabled = *count == 0; - let window = use_window::(&cx); + let window = use_window::(&cx); cx.render(rsx! { h1 { "Counter Example" } diff --git a/examples/desktop.rs b/examples/desktop.rs deleted file mode 100644 index e0d5993..0000000 --- a/examples/desktop.rs +++ /dev/null @@ -1,185 +0,0 @@ -use bevy::{ - app::App, - ecs::{ - component::Component, - event::{EventReader, EventWriter}, - query::With, - system::{Commands, Query}, - }, - input::keyboard::{KeyCode, KeyboardInput}, - log::{info, LogPlugin}, - time::TimePlugin, - window::{ReceivedCharacter, WindowCloseRequested, WindowDescriptor, WindowId}, -}; -use bevy_dioxus::desktop::prelude::*; -use dioxus::prelude::*; -use leafwing_input_manager::prelude::*; - -#[derive(Debug, Clone)] -enum CoreCommand { - Test, -} - -#[derive(Debug, Clone)] -enum UiCommand { - Test, - KeyboardInput(KeyboardInput), -} - -#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug)] -enum Action { - CloseWindow, -} - -#[derive(Component)] -struct User; - -fn main() { - App::new() - .insert_non_send_resource(DioxusSettings::<()> { - keyboard_event: true, - ..Default::default() - }) - .insert_resource(WindowDescriptor { - title: "Bevy Dioxus Plugin Demo".to_string(), - ..Default::default() - }) - .add_plugin(DioxusPlugin::::new(Root)) - .add_plugin(LogPlugin) - .add_plugin(TimePlugin) - .add_system(send_keyboard_input) - .add_system(handle_core_command) - .add_plugin(InputManagerPlugin::::default()) - .add_system(log_keyboard_event) - .add_startup_system(spawn_user) - .add_system(close_window) - .run(); -} - -fn handle_core_command(mut events: EventReader, mut ui: EventWriter) { - for cmd in events.iter() { - info!("🧠 {:?}", cmd); - - match cmd { - CoreCommand::Test => ui.send(UiCommand::Test), - } - } -} - -fn send_keyboard_input(mut events: EventReader, mut event: EventWriter) { - for input in events.iter() { - event.send(UiCommand::KeyboardInput(input.clone())); - } -} - -fn log_keyboard_event( - mut keyboard_input_events: EventReader, - mut received_character_events: EventReader, -) { - for input in keyboard_input_events.iter() { - info!("🧠 {:?}", input.clone()); - } - - for received_char in received_character_events.iter() { - info!("🧠 {:?}", received_char.clone()); - } -} - -#[allow(non_snake_case)] -fn Root(cx: Scope) -> Element { - let window = use_window::(&cx); - let input = use_state(&cx, || None); - let state = use_state(&cx, || None); - - use_future(&cx, (), |_| { - let input = input.clone(); - let state = state.clone(); - let rx = window.receiver(); - - async move { - while let Some(cmd) = rx.receive().await { - match cmd { - UiCommand::KeyboardInput(i) => { - input.set(i.key_code); - state.set(Some(i.state)); - } - _ => {} - } - } - } - }); - - cx.render(rsx! ( - div { - div { - h1 { "Bevy Dioxus Plugin Example" } - div { - p {"Test CoreCommand channel"} - button { - onclick: |_e| window.send(CoreCommand::Test), - "Test", - } - } - div { - p {"Close Window (press Esc or Ctrl + c)"} - button { - onclick: |_e| window.close(), - "Close", - } - } - div { - p { "Minimize Window" } - button { - onclick: move |_| window.set_minimized(true), - "Minimize" - } - } - } - - input.and_then(|input| Some(rsx!( - div { - h2 { "Keyboard Input" }, - div { - box_sizing: "border-box", - background: "#DCDCDC", - height: "4rem", - width: "100%", - display: "flex", - align_items: "center", - border_radius: "4px", - padding: "1rem", - code { - [format_args!("input: {:?}, state: {:?}", input, state.unwrap())], - } - } - } - ))) - } - )) -} - -fn spawn_user(mut commands: Commands) { - let mut input_map = InputMap::new([(KeyCode::Escape, Action::CloseWindow)]); - input_map.insert_chord([KeyCode::LControl, KeyCode::C], Action::CloseWindow); - input_map.insert_chord([KeyCode::RControl, KeyCode::C], Action::CloseWindow); - - commands - .spawn() - .insert(User) - .insert_bundle(InputManagerBundle:: { - action_state: ActionState::default(), - input_map, - }); -} - -fn close_window( - query: Query<&ActionState, With>, - mut events: EventWriter, -) { - let action_state = query.single(); - if action_state.just_pressed(Action::CloseWindow) { - events.send(WindowCloseRequested { - id: WindowId::primary(), - }); - } -} diff --git a/examples/global-state-ecs.rs b/examples/global-state-ecs.rs index 68ffb25..b3e00f4 100644 --- a/examples/global-state-ecs.rs +++ b/examples/global-state-ecs.rs @@ -64,7 +64,7 @@ fn update_global_state( #[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} !" } diff --git a/examples/keyboard-event.rs b/examples/keyboard-event.rs index 4ee99c1..ce75ef6 100644 --- a/examples/keyboard-event.rs +++ b/examples/keyboard-event.rs @@ -8,14 +8,22 @@ fn main() { keyboard_event: true, ..Default::default() }) + .add_plugin(DioxusPlugin::::new(Root)) + .add_plugin(GlobalStatePlugin) + .init_resource::() .add_plugin(LogPlugin) - .add_plugin(DioxusPlugin::::new(Root)) - .add_startup_system(setup) .add_system(handle_core_cmd) + .add_system(apply_global_state) .add_system(log_keyboard_event) .run(); } +#[global_state] +struct GlobalState { + event_type: EventType, + input_result: InputResult, +} + // Bevy Components #[derive(Component, Debug, Clone, Default)] struct SelectedType(EventType); @@ -23,11 +31,39 @@ struct SelectedType(EventType); // UI -> Core #[derive(Debug, Clone)] enum CoreCommand { - NewEventType(EventType), + EventType(EventType), +} + +impl CoreCommand { + fn keyboard_event() -> Self { + Self::EventType(EventType::KeyboardEvent) + } + + fn keyboard_input() -> Self { + Self::EventType(EventType::KeyboardInput) + } + + fn received_char() -> Self { + Self::EventType(EventType::ReceivedCharacter) + } +} + +#[derive(Clone, Debug)] +pub enum InputResult { + KeyboardEvent(KeyboardEvent), + KeyboardInput(KeyboardInput), + ReceivedCharacter(ReceivedCharacter), + None, +} + +impl Default for InputResult { + fn default() -> Self { + Self::None + } } -#[derive(Component, Debug, Clone, PartialEq)] -enum EventType { +#[derive(Clone, Debug)] +pub enum EventType { KeyboardEvent, KeyboardInput, ReceivedCharacter, @@ -42,89 +78,104 @@ impl Default for EventType { // UI Component #[allow(non_snake_case)] fn Root(cx: Scope) -> Element { - let event_type = use_state(&cx, || EventType::default()); - let window = use_window::(&cx); + let event_type = use_read(&cx, EVENT_TYPE); + let input_result = use_read(&cx, INPUT_RESULT); + let window = use_window::(&cx); - use EventType::*; cx.render(rsx! { h1 { "Keyboard Event Example" } p { "💡 Type any keys and checkout console. (TODO: You might need to click screen to focus.)" } div { - input { - r#type: "radio", - id: "keyboard-event", - checked: format_args!("{}", *event_type == KeyboardEvent), - onchange: |_e| { - event_type.modify(|_| KeyboardEvent); - window.send(CoreCommand::NewEventType(KeyboardEvent)); - }, - style: "margin: 0.5rem;", - } - label { - r#for: "keyboard-event", - style: "padding-right: 1rem;", - "KeyboardEvent", - } - input { - r#type: "radio", - id: "keyboard-input", - checked: format_args!("{}", *event_type == KeyboardInput), - onchange: |_e| { - event_type.modify(|_| KeyboardInput); - window.send(CoreCommand::NewEventType(KeyboardInput)); - }, - style: "margin: 0.5rem;", - } - label { - r#for: "keyboard-input", - style: "padding-right: 1rem;", - "KeyboardInput", - } - input { - r#type: "radio", - id: "received-character", - checked: format_args!("{}", *event_type == ReceivedCharacter), - onchange: |_e| { - event_type.modify(|_| ReceivedCharacter); - window.send(CoreCommand::NewEventType(ReceivedCharacter)); + select { + value: format_args!("{:?}", event_type), + onchange: |e| { + match e.value.as_str() { + "KeyboardEvent" => { + window.send(CoreCommand::keyboard_event()); + }, + "KeyboardInput" => { + window.send(CoreCommand::keyboard_input()); + }, + "ReceivedCharacter" => { + window.send(CoreCommand::received_char()); + } + _ => {} + }; }, - style: "margin: 0.5rem;", - } - label { - r#for: "received-character", - style: "padding-right: 1rem;", - "ReceivedCharacter", + + option { + value: "KeyboardEvent", + "KeyboardEvent" + } + option { + value: "KeyboardInput", + "KeyboardInput" + } + option { + value: "ReceivedCharacter", + "ReceivedCharacter" + } } } + + code { + [format_args!("Input result: {:#?}", input_result)], + } }) } -// Systems -fn setup(mut commands: Commands) { - commands.spawn().insert(SelectedType::default()); -} - -fn handle_core_cmd(mut events: EventReader, mut query: Query<&mut SelectedType>) { +fn handle_core_cmd(mut events: EventReader, mut event_type: ResMut) { for cmd in events.iter() { - let mut selected = query.single_mut(); match cmd { - CoreCommand::NewEventType(e) => { - info!("🧠 NewEventType: {:?}", e); - selected.0 = e.clone(); + CoreCommand::EventType(e) => { + info!("🧠 EventType: {:?}", e); + *event_type = e.clone(); } } } } +fn apply_global_state( + event_type: Res, + mut global_state: EventWriter, + mut keyboard_events: EventReader, + mut keyboard_inputs: EventReader, + mut received_characters: EventReader, +) { + if event_type.is_changed() { + global_state.send(GlobalState::EventType(event_type.clone())); + } + + match event_type.clone() { + EventType::KeyboardEvent => { + for e in keyboard_events.iter() { + global_state.send(GlobalState::InputResult(InputResult::KeyboardEvent(e.clone()))); + } + + } + EventType::KeyboardInput => { + for e in keyboard_inputs.iter() { + global_state.send(GlobalState::InputResult(InputResult::KeyboardInput(e.clone()))); + } + + } + EventType::ReceivedCharacter => { + for e in received_characters.iter() { + global_state.send(GlobalState::InputResult(InputResult::ReceivedCharacter(e.clone()))); + } + + } + }; +} + fn log_keyboard_event( mut keyboard_events: EventReader, mut keyboard_input_events: EventReader, mut received_character_events: EventReader, - query: Query<&SelectedType>, + event_type: Res, ) { - let selected = query.single(); - match selected.0 { + match *event_type { EventType::KeyboardEvent => { for event in keyboard_events.iter() { info!("🧠 {:?}", event.clone()); diff --git a/examples/root-props.rs b/examples/root-props.rs index 929cbd7..54db139 100644 --- a/examples/root-props.rs +++ b/examples/root-props.rs @@ -8,9 +8,7 @@ fn main() { title: "Props Example".to_string(), ..Default::default() }) - .add_plugin(DioxusPlugin::::new( - Root, - )) + .add_plugin(DioxusPlugin::::new(Root)) .add_plugin(LogPlugin) .run(); } diff --git a/examples/todomvc/main.rs b/examples/todomvc/main.rs index d42185b..d2b8c0f 100644 --- a/examples/todomvc/main.rs +++ b/examples/todomvc/main.rs @@ -7,12 +7,13 @@ mod system; mod ui; use crate::{channel::*, event::*, global_state::*, resource::*, system::*, ui::Root}; -use bevy::prelude::*; +use bevy::{log::LogPlugin, prelude::*}; use bevy_dioxus::desktop::prelude::*; fn main() { App::new() - .add_plugin(DioxusPlugin::::new(Root)) + .add_plugin(DioxusPlugin::::new(Root)) + .add_plugin(LogPlugin) .add_plugin(GlobalStatePlugin) .init_resource::() .add_event::() diff --git a/examples/todomvc/ui.rs b/examples/todomvc/ui.rs index 0e3e5e1..61f5324 100644 --- a/examples/todomvc/ui.rs +++ b/examples/todomvc/ui.rs @@ -5,7 +5,7 @@ use dioxus::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 settings = use_read(&cx, SETTINGS); @@ -90,6 +90,7 @@ pub fn Root(cx: Scope) -> Element { }, todo_list.iter().map(|todo| rsx! { li { + key: "{todo.entity:?}", style: "display: flex; align-items: center; justify-content: space-between; background: #ddd; padding: 1rem; height: 32px;", onmouseover: |_| { hovered.set(Some(todo.entity)); diff --git a/examples/window/multiple-windows.rs b/examples/window/multiple-windows.rs index ba8e2a7..5585e3b 100644 --- a/examples/window/multiple-windows.rs +++ b/examples/window/multiple-windows.rs @@ -10,7 +10,7 @@ use dioxus::prelude::*; fn main() { App::new() .add_plugin(LogPlugin) - .add_plugin(DioxusPlugin::::new(Root)) + .add_plugin(DioxusPlugin::::new(Root)) .add_event::() .add_system(create_new_window) .run(); @@ -31,7 +31,7 @@ fn create_new_window(mut events: EventReader, mut create: EventWriter #[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/window/render-mode.rs b/examples/window/render-mode.rs new file mode 100644 index 0000000..6680948 --- /dev/null +++ b/examples/window/render-mode.rs @@ -0,0 +1,139 @@ +use bevy::{ + log::{self, LogPlugin}, + prelude::*, + time::TimePlugin, +}; +use bevy_dioxus::desktop::prelude::*; +use dioxus::prelude::*; + +/// This example illustrates how to customize render setting +fn main() { + App::new() + .add_plugin(LogPlugin) + .add_plugin(TimePlugin) + .add_plugin(DioxusPlugin::::new(Root)) + .add_plugin(GlobalStatePlugin) + // .insert_non_send_resource(DioxusSettings::<()>::game()) + .init_resource::() + .init_resource::() + .add_system(increment_frame) + .add_system(handle_global_state_change) + .add_system(update_render_mode) + .run(); +} + +#[global_state] +struct GlobalState { + frame: u32, + render_mode: RenderMode, +} + +#[derive(Clone, Debug)] +enum CoreCommand { + RenderMode(RenderMode), +} + +impl CoreCommand { + fn application() -> Self { + Self::RenderMode(RenderMode::Application) + } + + fn game() -> Self { + Self::RenderMode(RenderMode::Game) + } +} + +#[derive(Default)] +struct Frame { + value: u32, +} + +#[derive(Component, Clone, Debug)] +pub enum RenderMode { + Application, + Game, +} + +impl Default for RenderMode { + fn default() -> Self { + Self::Application + } +} + +fn increment_frame(mut frame: ResMut) { + frame.value += 1; + log::debug!("update_frame system: frame: {}", frame.value); +} + +fn handle_global_state_change( + frame: Res, + render_mode: Res, + mut global_state: EventWriter, +) { + if frame.is_changed() { + global_state.send(GlobalState::Frame(frame.value)); + } + + if render_mode.is_changed() { + global_state.send(GlobalState::RenderMode(render_mode.clone())); + } +} + +fn update_render_mode( + mut events: EventReader, + mut render_mode: ResMut, + mut dioxus_settings: NonSendMut>, + mut global_state: EventWriter, +) { + for cmd in events.iter() { + match cmd { + CoreCommand::RenderMode(mode) => { + *render_mode = mode.clone(); + *dioxus_settings = match mode { + RenderMode::Application => DioxusSettings::application(), + RenderMode::Game => DioxusSettings::game(), + } + } + } + + global_state.send(GlobalState::RenderMode(render_mode.clone())); + } +} + +#[allow(non_snake_case)] +fn Root(cx: Scope) -> Element { + let window = use_window::(&cx); + + let frame = use_read(&cx, FRAME); + let render_mode = use_read(&cx, RENDER_MODE); + + cx.render(rsx! { + h1 { "Window: Render Mode" } + + select { + value: format_args!("{:?}", render_mode), + onchange: |e| { + log::debug!("onchange: {:#?}", e.value.as_str()); + match e.value.as_str() { + "Application" => { window.send(CoreCommand::application()) } + "Game" => { window.send(CoreCommand::game()) } + _ => {} + } + }, + option { + value: "Application", + "Application" + } + option { + value: "Game", + "Game" + } + } + + div { + style: "background: #ddd; padding: 1rem;", + p { [format_args!("Mode: {:?}", render_mode)] } + p { [format_args!("Frame: {}", frame)] } + } + }) +} diff --git a/examples/window/update-mode.rs b/examples/window/update-mode.rs deleted file mode 100644 index 5ac2a67..0000000 --- a/examples/window/update-mode.rs +++ /dev/null @@ -1,131 +0,0 @@ -use bevy::{log::LogPlugin, prelude::*, time::TimePlugin}; -use bevy_dioxus::desktop::prelude::*; -use dioxus::prelude::*; -use leafwing_input_manager::prelude::*; - -/// This example illustrates how to customize render setting -fn main() { - App::new() - .insert_non_send_resource(DioxusSettings::<()> { - keyboard_event: true, - ..Default::default() - }) - .add_plugin(LogPlugin) - .add_plugin(TimePlugin) - .add_plugin(DioxusPlugin::::new(Root)) - .add_plugin(InputManagerPlugin::::default()) - .add_startup_system(setup) - .add_system(update_frame) - .add_system(toggle_update_mode) - .add_system(handle_mode_update) - .run(); -} - -#[derive(Component)] -struct User; - -#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug)] -enum Action { - ToggleUpdateMode, -} - -#[derive(Component, Clone, Debug, Default)] -struct Frame(u32); - -#[derive(Component, Clone, Debug)] -enum Mode { - Application, - Game, -} - -impl Default for Mode { - fn default() -> Self { - Self::Application - } -} - -#[derive(Clone, Debug)] -enum UiCommand { - Frame(u32), - Mode(Mode), -} - -fn setup(mut commands: Commands, mut ui: EventWriter) { - let frame = Frame::default(); - commands.spawn().insert(Frame::default()); - commands.spawn().insert(Mode::default()); - commands - .spawn() - .insert(User) - .insert_bundle(InputManagerBundle:: { - action_state: ActionState::default(), - input_map: InputMap::new([(KeyCode::Space, Action::ToggleUpdateMode)]), - }); - - ui.send(UiCommand::Frame(frame.0)); -} - -fn update_frame(mut query: Query<&mut Frame>, mut ui: EventWriter) { - let mut frame = query.single_mut(); - frame.0 += 1; - ui.send(UiCommand::Frame(frame.0)); -} - -fn toggle_update_mode(query: Query<&ActionState, With>, mut modes: Query<&mut Mode>) { - let action_state = query.single(); - if action_state.just_pressed(Action::ToggleUpdateMode) { - let mut mode = modes.single_mut(); - *mode = match *mode { - Mode::Application => Mode::Game, - Mode::Game => Mode::Application, - }; - } -} - -fn handle_mode_update( - query: Query<&Mode, Changed>, - mut settings: NonSendMut>, - mut events: EventWriter, -) { - for mode in query.iter() { - *settings = match *mode { - Mode::Application => DioxusSettings::desktop_app(), - Mode::Game => DioxusSettings::game(), - }; - events.send(UiCommand::Mode(mode.clone())); - } -} - -#[allow(non_snake_case)] -fn Root(cx: Scope) -> Element { - let window = use_window::<(), UiCommand>(&cx); - let frame = use_state(&cx, || 0); - let mode = use_state(&cx, || Mode::Application); - - use_future(&cx, (), |_| { - let rx = window.receiver(); - let frame = frame.clone(); - let mode = mode.clone(); - - async move { - while let Some(cmd) = rx.receive().await { - match cmd { - UiCommand::Frame(f) => frame.modify(|_| f), - UiCommand::Mode(m) => mode.modify(|_| m), - } - } - } - }); - - cx.render(rsx! { - h1 { "Window: Update Mode" } - p { "💡 Press \"Space\" to toggle mode. (TODO: You might need to click screen to focus.)" } - p { "The frame gets updated only when user event occurs or with max timeout on Application mode." } - p { "Try press any keys or change window size to see frame increments." } - div { - style: "background: #ddd; padding: 1rem;", - p { [format_args!("Mode: {:?}", mode)] } - p { [format_args!("Frame: {}", frame)] } - } - }) -} diff --git a/packages/desktop/src/context.rs b/packages/desktop/src/context.rs index 4fc399a..f1e61b5 100644 --- a/packages/desktop/src/context.rs +++ b/packages/desktop/src/context.rs @@ -2,37 +2,28 @@ use crate::event::{ UiEvent::{self, *}, WindowEvent::*, }; -use futures_intrusive::channel::shared::{Receiver, Sender}; +use futures_intrusive::channel::shared::Sender; use std::fmt::Debug; use wry::application::event_loop::EventLoopProxy; pub type ProxyType = EventLoopProxy>; #[derive(Clone)] -pub struct UiContext { +pub struct UiContext { proxy: ProxyType, - channel: (Sender, Receiver), + core_tx: Sender, } -impl UiContext +impl UiContext where CoreCommand: Debug + Clone, - UiCommand: Debug + Clone, { - pub fn new( - proxy: ProxyType, - channel: (Sender, Receiver), - ) -> Self { - Self { proxy, channel } - } - - pub fn receiver(&self) -> Receiver { - self.channel.1.clone() + pub fn new(proxy: ProxyType, core_tx: Sender) -> Self { + Self { proxy, core_tx } } pub fn send(&self, cmd: CoreCommand) { - self.channel - .0 + self.core_tx .try_send(cmd) .expect("Failed to send CoreCommand"); } @@ -104,4 +95,10 @@ where pub fn eval(&self, script: impl std::string::ToString) { let _ = self.proxy.send_event(WindowEvent(Eval(script.to_string()))); } + + pub fn rerender(&self) { + self.proxy + .send_event(UiEvent::WindowEvent(Rerender)) + .unwrap(); + } } diff --git a/packages/desktop/src/event.rs b/packages/desktop/src/event.rs index d5e0392..f0b5a47 100644 --- a/packages/desktop/src/event.rs +++ b/packages/desktop/src/event.rs @@ -81,7 +81,7 @@ pub(crate) fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { #[derive(Debug)] pub enum WindowEvent { /// When VirtualDOM applies all edits - Update, + Rerender, /// When close window is requested CloseWindow, @@ -129,7 +129,7 @@ pub enum WindowEvent { /// Event to control VirtualDom from outside #[derive(Debug)] -pub enum VDomCommand { +pub enum VirtualDomCommand { /// Apply all edits UpdateDom, diff --git a/packages/desktop/src/runner.rs b/packages/desktop/src/event_loop.rs similarity index 89% rename from packages/desktop/src/runner.rs rename to packages/desktop/src/event_loop.rs index 7d03dc1..9d08941 100644 --- a/packages/desktop/src/runner.rs +++ b/packages/desktop/src/event_loop.rs @@ -15,44 +15,29 @@ use bevy::{ utils::Instant, window::{ CreateWindow, FileDragAndDrop, ReceivedCharacter, RequestRedraw, - WindowBackendScaleFactorChanged, WindowCloseRequested, WindowFocused, WindowId, WindowMode, - WindowMoved, WindowResized, WindowScaleFactorChanged, Windows, + WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowFocused, + WindowId, WindowMode, WindowMoved, WindowResized, WindowScaleFactorChanged, Windows, }, }; -use futures_intrusive::channel::shared::Receiver; use std::fmt::Debug; -use tokio::runtime::Runtime; use wry::application::{ dpi::LogicalSize, event::{DeviceEvent, Event, StartCause, WindowEvent as TaoWindowEvent}, event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, }; -pub fn runner(mut app: App) +pub fn start_event_loop(mut app: App) where CoreCommand: 'static + Send + Sync + Clone + Debug, - UiCommand: 'static + Send + Sync + Clone + Debug, Props: 'static + Send + Sync + Clone + Default, { let event_loop = app .world .remove_non_send_resource::>>() .unwrap(); - let core_rx = app - .world - .remove_resource::>() - .unwrap(); - let runtime = app.world.get_resource::().unwrap(); - let proxy = event_loop.create_proxy(); let mut tao_state = TaoPersistentState::default(); - runtime.spawn(async move { - while let Some(cmd) = core_rx.receive().await { - proxy.clone().send_event(UiEvent::CoreCommand(cmd)).unwrap(); - } - }); - event_loop.run( move |event: Event>, _event_loop: &EventLoopWindowTarget>, @@ -60,8 +45,9 @@ where log::debug!("{event:?}"); match event { Event::NewEvents(start) => { - let dioxus_settings = app.world.non_send_resource::>(); - let windows = app.world.resource::(); + let world = app.world.cell(); + let dioxus_settings = world.non_send_resource::>(); + let windows = world.resource::(); let focused = windows.iter().any(|w| w.is_focused()); let auto_timeout_reached = matches!(start, StartCause::ResumeTimeReached { .. }); @@ -81,6 +67,7 @@ where window_id: tao_window_id, .. } => { + tao_state.prevent_app_update = false; let world = app.world.cell(); let dioxus_windows = world.get_non_send_resource_mut::().unwrap(); @@ -243,6 +230,7 @@ where } } Event::UserEvent(user_event) => { + tao_state.prevent_app_update = false; match user_event { UiEvent::WindowEvent(window_event) => { let world = app.world.cell(); @@ -255,9 +243,10 @@ where let tao_window = dioxus_windows.get_tao_window(id).unwrap(); match window_event { - WindowEvent::Update => { + WindowEvent::Rerender => { + tao_state.prevent_app_update = true; let dioxus_window = dioxus_windows.get_mut(id).unwrap(); - dioxus_window.update(); + dioxus_window.rerender(); } WindowEvent::CloseWindow => { let mut events = world @@ -382,6 +371,7 @@ where event: DeviceEvent::MouseMotion { delta, .. }, .. } => { + tao_state.prevent_app_update = false; let mut mouse_motion_events = app.world.resource_mut::>(); mouse_motion_events.send(MouseMotion { delta: Vec2::new(delta.0 as f32, delta.1 as f32), @@ -394,21 +384,22 @@ where tao_state.active = true; } Event::MainEventsCleared => { - handle_create_window_events::(&mut app.world); + handle_create_window_events::(&mut app.world); let dioxus_settings = app.world.non_send_resource::>(); - let update = if tao_state.active { + let update = if !tao_state.active { + false + } else { let windows = app.world.resource::(); let focused = windows.iter().any(|w| w.is_focused()); match dioxus_settings.update_mode(focused) { UpdateMode::Continuous { .. } => true, - UpdateMode::Reactive { .. } | UpdateMode::ReactiveLowPower { .. } => { + UpdateMode::Reactive { .. } => !tao_state.prevent_app_update, + UpdateMode::ReactiveLowPower { .. } => { tao_state.low_power_event || tao_state.redraw_request_sent || tao_state.timeout_reached } } - } else { - false }; if update { @@ -418,20 +409,24 @@ where } } Event::RedrawEventsCleared => { - { - let dioxus_settings = - app.world.non_send_resource::>(); - let windows = app.world.non_send_resource::(); - let focused = windows.iter().any(|w| w.is_focused()); - let now = Instant::now(); - use UpdateMode::*; - *control_flow = match dioxus_settings.update_mode(focused) { - Continuous => ControlFlow::Poll, - Reactive { max_wait } | ReactiveLowPower { max_wait } => { - ControlFlow::WaitUntil(now + *max_wait) - } - }; - } + log::debug!(""); + tao_state.prevent_app_update = true; + + let dioxus_settings = app.world.non_send_resource::>(); + let windows = app.world.non_send_resource::(); + let focused = windows.iter().any(|w| w.is_focused()); + let now = Instant::now(); + use UpdateMode::*; + *control_flow = match dioxus_settings.update_mode(focused) { + Continuous => ControlFlow::Poll, + Reactive { max_wait } | ReactiveLowPower { max_wait } => { + ControlFlow::WaitUntil(now + *max_wait) + } + }; + + // This block needs to run after `app.update()` in `MainEventsCleared`. Otherwise, + // we won't be able to see redraw requests until the next event, defeating the + // purpose of a redraw request! let mut redraw = false; if let Some(app_redraw_events) = app.world.get_resource::>() @@ -457,30 +452,29 @@ where ); } -fn handle_create_window_events(world: &mut World) +fn handle_create_window_events(world: &mut World) where CoreCommand: 'static + Send + Sync + Clone + Debug, - UiCommand: 'static + Send + Sync + Clone + Debug, Props: 'static + Send + Sync + Clone, { let world = world.cell(); - // let mut dioxus_windows = world.get_non_send_mut::().unwrap(); - // let mut windows = world.get_resource_mut::().unwrap(); + let mut dioxus_windows = world.get_non_send_resource_mut::().unwrap(); + let mut windows = world.get_resource_mut::().unwrap(); let create_window_events = world.get_resource::>().unwrap(); let mut create_window_events_reader = ManualEventReader::::default(); - // let mut window_created_events = world.get_resource_mut::>().unwrap(); + let mut window_created_events = world.get_resource_mut::>().unwrap(); - for _create_window_event in create_window_events_reader.iter(&create_window_events) { + 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::( - // &world, - // create_window_event.id, - // &create_window_event.descriptor, - // ); - // windows.add(window); - // window_created_events.send(WindowCreated { - // id: create_window_event.id, - // }); + let window = dioxus_windows.create::( + &world, + create_window_event.id, + &create_window_event.descriptor, + ); + windows.add(window); + window_created_events.send(WindowCreated { + id: create_window_event.id, + }); } } @@ -489,6 +483,7 @@ struct TaoPersistentState { low_power_event: bool, redraw_request_sent: bool, timeout_reached: bool, + prevent_app_update: bool, last_update: Instant, } @@ -499,6 +494,7 @@ impl Default for TaoPersistentState { low_power_event: false, redraw_request_sent: false, timeout_reached: false, + prevent_app_update: true, last_update: Instant::now(), } } diff --git a/packages/desktop/src/hooks.rs b/packages/desktop/src/hooks.rs index 93a5510..be56dc0 100644 --- a/packages/desktop/src/hooks.rs +++ b/packages/desktop/src/hooks.rs @@ -5,23 +5,21 @@ 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 CoreCommand: Debug + Clone, - UiCommand: Clone + 'static, { - cx.use_hook(|_| cx.consume_context::>()) + cx.use_hook(|_| cx.consume_context::>()) .as_ref() - .expect("Failed to find UiContext, check CoreCommand and UiCommand type parameter") + .expect("Failed to find UiContext, check CoreCommand 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 CoreCommand: Debug + Clone + 'static, - UiCommand: Clone + 'static + Debug, { - 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/lib.rs b/packages/desktop/src/lib.rs index d0a21cf..971cc3e 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -5,12 +5,14 @@ mod context; mod converter; pub mod event; +mod event_loop; pub mod hooks; pub mod plugin; mod protocol; -mod runner; pub mod setting; pub mod stage; +mod system; +mod virtual_dom; mod window; pub mod prelude { diff --git a/packages/desktop/src/plugin.rs b/packages/desktop/src/plugin.rs index 89eb14b..1bb3dbe 100644 --- a/packages/desktop/src/plugin.rs +++ b/packages/desktop/src/plugin.rs @@ -3,11 +3,12 @@ use crate::{ context::UiContext, - converter, - event::{KeyboardEvent, UiEvent, VDomCommand, WindowEvent}, - runner::runner, + event::{KeyboardEvent, UiEvent, VirtualDomCommand}, + event_loop::start_event_loop, setting::DioxusSettings, stage::UiStage, + system::change_window, + virtual_dom::VirtualDom, window::DioxusWindows, }; @@ -15,91 +16,96 @@ use bevy::{ app::prelude::*, ecs::{event::Events, prelude::*}, input::InputPlugin, - log::error, - log::warn, - math::{UVec2, Vec2}, - window::{ - CreateWindow, ModifiesWindows, WindowClosed, WindowCommand, WindowCreated, WindowMode, - WindowPlugin, WindowScaleFactorChanged, Windows, - }, + window::{CreateWindow, ModifiesWindows, WindowCreated, WindowPlugin, Windows}, }; use bevy_dioxus_core::prelude::GlobalStateHandler; -use dioxus_core::{Component as DioxusComponent, SchedulerMsg, VirtualDom}; -use fermi::AtomRoot; +use dioxus_core::{Component as DioxusComponent, SchedulerMsg}; use futures_channel::mpsc; -use futures_intrusive::channel::{ - shared::{channel, Receiver, Sender}, - TrySendError, -}; -use std::{ - fmt::Debug, - marker::PhantomData, - rc::Rc, - sync::{Arc, Mutex}, -}; -use tokio::{runtime::Runtime, select}; -use wry::application::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, - event_loop::EventLoop, - window::Fullscreen, -}; +use futures_intrusive::channel::shared::channel; +use std::{fmt::Debug, marker::PhantomData, sync::Arc, sync::Mutex}; +use tokio::runtime::Runtime; +use wry::application::event_loop::EventLoop; /// Dioxus Plugin for Bevy -pub struct DioxusPlugin { +pub struct DioxusPlugin { /// Root component pub Root: DioxusComponent, global_state_type: PhantomData, core_cmd_type: PhantomData, - ui_cmd_type: PhantomData, } -impl Plugin - for DioxusPlugin +impl Plugin for DioxusPlugin where GlobalState: 'static + Send + Sync + GlobalStateHandler, CoreCommand: 'static + Send + Sync + Clone + Debug, - UiCommand: 'static + Send + Sync + Clone + Debug, Props: 'static + Send + Sync + Clone + Default, { fn build(&self, app: &mut App) { + let (vdom_scheduler_tx, vdom_scheduler_rx) = mpsc::unbounded::(); + let (vdom_command_tx, vdom_command_rx) = channel::>(8); let (core_tx, core_rx) = channel::(8); - let (ui_tx, ui_rx) = channel::(8); - let (vdom_cmd_tx, vdom_cmd_rx) = channel::>(8); + let event_loop = EventLoop::>::with_user_event(); let settings = app .world .remove_non_send_resource::>() .unwrap_or_default(); + let proxy = event_loop.create_proxy(); + let edit_queue = Arc::new(Mutex::new(Vec::new())); + + let runtime = Runtime::new().unwrap(); + + let proxy_clone = proxy.clone(); + runtime.spawn(async move { + while let Some(cmd) = core_rx.clone().receive().await { + log::debug!("CoreCommand: {:#?}", cmd); + proxy_clone.send_event(UiEvent::CoreCommand(cmd)).unwrap(); + } + }); + + let root_clone = self.Root.clone(); + let props_clone = settings.props.as_ref().unwrap().clone(); + let edit_queue_clone = edit_queue.clone(); + let vdom_scheduler_tx_clone = vdom_scheduler_tx.clone(); app.add_plugin(WindowPlugin::default()) .add_plugin(UiStagePlugin) .add_plugin(InputPlugin) .add_event::() .add_event::() - .add_event::() - .insert_resource(core_rx) - .insert_resource(ui_tx) - .insert_resource(Runtime::new().unwrap()) - .insert_resource(vdom_cmd_tx) - .insert_non_send_resource(settings) + .insert_resource(runtime) + .insert_resource(vdom_scheduler_tx) + .insert_resource(vdom_command_tx) + .insert_resource(edit_queue) .init_non_send_resource::() - .insert_non_send_resource(EventLoop::>::with_user_event()) - .set_runner(|app| runner::(app)) - .add_system_to_stage(UiStage::Update, send_ui_commands::) + .insert_non_send_resource(settings) + .insert_non_send_resource(event_loop) + .set_runner(|app| start_event_loop::(app)) .add_system_to_stage(UiStage::Update, change_window.label(ModifiesWindows)); - self.spawn_virtual_dom(&mut app.world, (core_tx, ui_rx), vdom_cmd_rx); + std::thread::spawn(move || { + Runtime::new().unwrap().block_on(async move { + let mut virtual_dom = VirtualDom::new( + root_clone, + props_clone, + edit_queue_clone, + (vdom_scheduler_tx_clone, vdom_scheduler_rx), + vdom_command_rx, + ); + virtual_dom.provide_ui_context(UiContext::new(proxy.clone(), core_tx)); + virtual_dom.run().await; + }); + }); + Self::handle_initial_window_events(&mut app.world); } } -impl - DioxusPlugin +impl DioxusPlugin where GlobalState: Send + Sync + GlobalStateHandler, CoreCommand: Clone + Debug + Send + Sync, - UiCommand: Clone + Debug + Send + Sync, Props: Send + Sync + Clone + 'static, { /// Initialize DioxusPlugin with root component and channel types @@ -111,11 +117,10 @@ where /// /// // DioxusPlugin accepts any types as command. Pass empty tuple if channel is not necessary. /// type CoreCommand = (); - /// type UiCommand = (); /// /// fn main() { /// App::new() - /// .add_plugin(DioxusPlugin::::new(Root)) + /// .add_plugin(DioxusPlugin::::new(Root)) /// .run(); /// } /// @@ -129,89 +134,10 @@ where Self { Root, core_cmd_type: PhantomData, - ui_cmd_type: PhantomData, global_state_type: PhantomData, } } - fn spawn_virtual_dom( - &self, - world: &mut World, - (core_tx, ui_rx): (Sender, Receiver), - vdom_cmd_rx: Receiver>, - ) { - let (dom_tx, dom_rx) = mpsc::unbounded::(); - let edit_queue = Arc::new(Mutex::new(Vec::new())); - let settings = world - .get_non_send_resource::>() - .unwrap(); - let Root = self.Root.clone(); - let props = settings.props.as_ref().unwrap().clone(); - let event_loop = world - .get_non_send_resource::>>() - .unwrap(); - let proxy = event_loop.create_proxy(); - let context = UiContext::::new(proxy.clone(), (core_tx, ui_rx)); - - world.insert_resource(dom_tx.clone()); - world.insert_resource(edit_queue.clone()); - - std::thread::spawn(move || { - // initialize vdom - let mut vdom = VirtualDom::new_with_props_and_scheduler(Root, props, (dom_tx, dom_rx)); - - // set UI context - vdom.base_scope().provide_context(context.clone()); - - // apply initial edit - let initial_muts = vdom.rebuild(); - edit_queue - .lock() - .unwrap() - .push(serde_json::to_string(&initial_muts.edits).unwrap()); - proxy - .send_event(UiEvent::WindowEvent(WindowEvent::Update)) - .unwrap(); - - let cx = vdom.base_scope(); - let root = match cx.consume_context::>() { - Some(root) => root, - None => cx.provide_root_context(Rc::new(AtomRoot::new(cx.schedule_update_any()))), - }; - - Runtime::new().unwrap().block_on(async move { - loop { - // wait for either - select! { - () = vdom.wait_for_work() => {} // 1) when event listener is triggered - cmd = vdom_cmd_rx.receive() => { // 2) when global state is changed or injected window.document event is emitted - if let Some(cmd) = cmd { - match cmd { - VDomCommand::UpdateDom => {} - VDomCommand::GlobalState(state) => { - state.handler(root.clone()) - } - } - } - } - } - - let muts = vdom.work_with_deadline(|| false); - for edit in muts { - edit_queue - .lock() - .unwrap() - .push(serde_json::to_string(&edit.edits).unwrap()); - } - - proxy - .send_event(UiEvent::WindowEvent(WindowEvent::Update)) - .unwrap(); - } - }); - }); - } - fn handle_initial_window_events(world: &mut World) where CoreCommand: 'static + Send + Sync + Clone + Debug, @@ -237,174 +163,6 @@ where } } -fn send_ui_commands(mut events: EventReader, tx: Res>) -where - UiCommand: 'static + Send + Sync + Clone + Debug, -{ - for cmd in events.iter() { - match tx.try_send(cmd.clone()) { - Ok(()) => {} - Err(e) => match e { - TrySendError::Full(e) => { - error!("Failed to send UiCommand: channel is full: event: {:?}", e); - } - TrySendError::Closed(e) => { - error!( - "Failed to send UiCommand: channel is closed: event: {:?}", - e - ); - } - }, - } - } -} - -fn change_window( - mut dioxus_windows: NonSendMut, - mut windows: ResMut, - mut window_dpi_changed_events: EventWriter, - mut window_close_events: EventWriter, -) { - let mut removed_windows = vec![]; - for bevy_window in windows.iter_mut() { - let id = bevy_window.id(); - let window = dioxus_windows.get_tao_window(id).unwrap(); - - for command in bevy_window.drain_commands() { - match command { - WindowCommand::SetWindowMode { - mode, - resolution: UVec2 { x, y }, - } => match mode { - WindowMode::BorderlessFullscreen => { - window.set_fullscreen(Some(Fullscreen::Borderless(None))); - } - WindowMode::Fullscreen => { - window.set_fullscreen(Some(Fullscreen::Exclusive( - DioxusWindows::get_best_videomode(&window.current_monitor().unwrap()), - ))); - } - WindowMode::SizedFullscreen => window.set_fullscreen(Some( - Fullscreen::Exclusive(DioxusWindows::get_fitting_videomode( - &window.current_monitor().unwrap(), - x, - y, - )), - )), - WindowMode::Windowed => window.set_fullscreen(None), - }, - WindowCommand::SetTitle { title } => { - window.set_title(&title); - } - WindowCommand::SetScaleFactor { scale_factor } => { - window_dpi_changed_events.send(WindowScaleFactorChanged { id, scale_factor }); - } - WindowCommand::SetResolution { - logical_resolution: - Vec2 { - x: width, - y: height, - }, - scale_factor, - } => { - window.set_inner_size( - LogicalSize::new(width, height).to_physical::(scale_factor), - ); - } - WindowCommand::SetPresentMode { .. } => (), - WindowCommand::SetResizable { resizable } => { - window.set_resizable(resizable); - } - WindowCommand::SetDecorations { decorations } => { - window.set_decorations(decorations); - } - WindowCommand::SetCursorIcon { icon } => { - window.set_cursor_icon(converter::convert_cursor_icon(icon)); - } - WindowCommand::SetCursorLockMode { locked } => { - window - .set_cursor_grab(locked) - .unwrap_or_else(|e| error!("Unable to un/grab cursor: {}", e)); - } - WindowCommand::SetCursorVisibility { visible } => { - window.set_cursor_visible(visible); - } - WindowCommand::SetCursorPosition { position } => { - let inner_size = window.inner_size().to_logical::(window.scale_factor()); - window - .set_cursor_position(LogicalPosition::new( - position.x, - inner_size.height - position.y, - )) - .unwrap_or_else(|e| error!("Unable to set cursor position: {}", e)); - } - WindowCommand::SetMaximized { maximized } => { - window.set_maximized(maximized); - } - WindowCommand::SetMinimized { minimized } => { - window.set_minimized(minimized); - } - WindowCommand::SetPosition { position } => { - window.set_outer_position(PhysicalPosition { - x: position[0], - y: position[1], - }); - } - WindowCommand::Center(monitor_selection) => { - use bevy::window::MonitorSelection::*; - let maybe_monitor = match monitor_selection { - Current => window.current_monitor(), - Primary => window.primary_monitor(), - Number(n) => window.available_monitors().nth(n), - }; - - if let Some(monitor) = maybe_monitor { - let screen_size = monitor.size(); - - let window_size = window.outer_size(); - - window.set_outer_position(PhysicalPosition { - x: screen_size.width.saturating_sub(window_size.width) as f64 / 2. - + monitor.position().x as f64, - y: screen_size.height.saturating_sub(window_size.height) as f64 / 2. - + monitor.position().y as f64, - }); - } else { - warn!("Couldn't get monitor selected with: {monitor_selection:?}"); - } - } - WindowCommand::SetResizeConstraints { resize_constraints } => { - let constraints = resize_constraints.check_constraints(); - let min_inner_size = LogicalSize { - width: constraints.min_width, - height: constraints.min_height, - }; - let max_inner_size = LogicalSize { - width: constraints.max_width, - height: constraints.max_height, - }; - - window.set_min_inner_size(Some(min_inner_size)); - if constraints.max_width.is_finite() && constraints.max_height.is_finite() { - window.set_max_inner_size(Some(max_inner_size)); - } - } - WindowCommand::Close => { - removed_windows.push(id); - break; - } - } - } - } - - if !removed_windows.is_empty() { - for id in removed_windows { - let _ = dioxus_windows.remove(id); - windows.remove(id); - window_close_events.send(WindowClosed { id }); - } - } -} struct UiStagePlugin; impl Plugin for UiStagePlugin { diff --git a/packages/desktop/src/setting.rs b/packages/desktop/src/setting.rs index 41710c5..3e84e7f 100644 --- a/packages/desktop/src/setting.rs +++ b/packages/desktop/src/setting.rs @@ -72,7 +72,7 @@ where } /// Configure tao with common settings for a desktop application. - pub fn desktop_app() -> Self { + pub fn application() -> Self { DioxusSettings { focused_mode: UpdateMode::Reactive { max_wait: Duration::from_secs(5), @@ -200,7 +200,7 @@ where Props: Default, { fn default() -> Self { - Self::desktop_app() + Self::application() } } diff --git a/packages/desktop/src/system.rs b/packages/desktop/src/system.rs new file mode 100644 index 0000000..3042ba4 --- /dev/null +++ b/packages/desktop/src/system.rs @@ -0,0 +1,161 @@ +use crate::{converter, window::DioxusWindows}; +use bevy::{ + ecs::{ + event::EventWriter, + system::{NonSendMut, ResMut}, + }, + log::{error, warn}, + math::{UVec2, Vec2}, + window::{WindowClosed, WindowCommand, WindowMode, WindowScaleFactorChanged, Windows}, +}; +use wry::application::{ + dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, + window::Fullscreen, +}; + +pub fn change_window( + mut dioxus_windows: NonSendMut, + mut windows: ResMut, + mut window_dpi_changed_events: EventWriter, + mut window_close_events: EventWriter, +) { + let mut removed_windows = vec![]; + for bevy_window in windows.iter_mut() { + let id = bevy_window.id(); + let window = dioxus_windows.get_tao_window(id).unwrap(); + + for command in bevy_window.drain_commands() { + match command { + WindowCommand::SetWindowMode { + mode, + resolution: UVec2 { x, y }, + } => match mode { + WindowMode::BorderlessFullscreen => { + window.set_fullscreen(Some(Fullscreen::Borderless(None))); + } + WindowMode::Fullscreen => { + window.set_fullscreen(Some(Fullscreen::Exclusive( + DioxusWindows::get_best_videomode(&window.current_monitor().unwrap()), + ))); + } + WindowMode::SizedFullscreen => window.set_fullscreen(Some( + Fullscreen::Exclusive(DioxusWindows::get_fitting_videomode( + &window.current_monitor().unwrap(), + x, + y, + )), + )), + WindowMode::Windowed => window.set_fullscreen(None), + }, + WindowCommand::SetTitle { title } => { + window.set_title(&title); + } + WindowCommand::SetScaleFactor { scale_factor } => { + window_dpi_changed_events.send(WindowScaleFactorChanged { id, scale_factor }); + } + WindowCommand::SetResolution { + logical_resolution: + Vec2 { + x: width, + y: height, + }, + scale_factor, + } => { + window.set_inner_size( + LogicalSize::new(width, height).to_physical::(scale_factor), + ); + } + WindowCommand::SetPresentMode { .. } => (), + WindowCommand::SetResizable { resizable } => { + window.set_resizable(resizable); + } + WindowCommand::SetDecorations { decorations } => { + window.set_decorations(decorations); + } + WindowCommand::SetCursorIcon { icon } => { + window.set_cursor_icon(converter::convert_cursor_icon(icon)); + } + WindowCommand::SetCursorLockMode { locked } => { + window + .set_cursor_grab(locked) + .unwrap_or_else(|e| error!("Unable to un/grab cursor: {}", e)); + } + WindowCommand::SetCursorVisibility { visible } => { + window.set_cursor_visible(visible); + } + WindowCommand::SetCursorPosition { position } => { + let inner_size = window.inner_size().to_logical::(window.scale_factor()); + window + .set_cursor_position(LogicalPosition::new( + position.x, + inner_size.height - position.y, + )) + .unwrap_or_else(|e| error!("Unable to set cursor position: {}", e)); + } + WindowCommand::SetMaximized { maximized } => { + window.set_maximized(maximized); + } + WindowCommand::SetMinimized { minimized } => { + window.set_minimized(minimized); + } + WindowCommand::SetPosition { position } => { + window.set_outer_position(PhysicalPosition { + x: position[0], + y: position[1], + }); + } + WindowCommand::Center(monitor_selection) => { + use bevy::window::MonitorSelection::*; + let maybe_monitor = match monitor_selection { + Current => window.current_monitor(), + Primary => window.primary_monitor(), + Number(n) => window.available_monitors().nth(n), + }; + + if let Some(monitor) = maybe_monitor { + let screen_size = monitor.size(); + + let window_size = window.outer_size(); + + window.set_outer_position(PhysicalPosition { + x: screen_size.width.saturating_sub(window_size.width) as f64 / 2. + + monitor.position().x as f64, + y: screen_size.height.saturating_sub(window_size.height) as f64 / 2. + + monitor.position().y as f64, + }); + } else { + warn!("Couldn't get monitor selected with: {monitor_selection:?}"); + } + } + WindowCommand::SetResizeConstraints { resize_constraints } => { + let constraints = resize_constraints.check_constraints(); + let min_inner_size = LogicalSize { + width: constraints.min_width, + height: constraints.min_height, + }; + let max_inner_size = LogicalSize { + width: constraints.max_width, + height: constraints.max_height, + }; + + window.set_min_inner_size(Some(min_inner_size)); + if constraints.max_width.is_finite() && constraints.max_height.is_finite() { + window.set_max_inner_size(Some(max_inner_size)); + } + } + WindowCommand::Close => { + removed_windows.push(id); + break; + } + } + } + } + + if !removed_windows.is_empty() { + for id in removed_windows { + let _ = dioxus_windows.remove(id); + windows.remove(id); + window_close_events.send(WindowClosed { id }); + } + } +} diff --git a/packages/desktop/src/virtual_dom.rs b/packages/desktop/src/virtual_dom.rs new file mode 100644 index 0000000..aa3ae1e --- /dev/null +++ b/packages/desktop/src/virtual_dom.rs @@ -0,0 +1,129 @@ +#![allow(non_snake_case)] + +use crate::{context::UiContext, event::VirtualDomCommand}; +use bevy_dioxus_core::GlobalStateHandler; +use dioxus_core::{Component, SchedulerMsg, VirtualDom as DioxusVirtualDom}; +use dioxus_hooks::{UnboundedReceiver, UnboundedSender}; +use fermi::AtomRoot; +use futures_intrusive::channel::shared::Receiver; +use std::{ + fmt::Debug, + marker::PhantomData, + rc::Rc, + sync::{Arc, Mutex}, +}; +use tokio::select; + +pub struct VirtualDom { + virtual_dom: DioxusVirtualDom, + edit_queue: Arc>>, + command_rx: Receiver>, + core_cmd_type: PhantomData, +} + +impl VirtualDom +where + GlobalState: GlobalStateHandler, + CoreCommand: 'static + Clone + Debug, +{ + pub fn new( + Root: Component, + props: Props, + edit_queue: Arc>>, + (scheduler_tx, scheduler_rx): ( + UnboundedSender, + UnboundedReceiver, + ), + command_rx: Receiver>, + ) -> Self + where + Props: 'static, + { + let virtual_dom = DioxusVirtualDom::new_with_props_and_scheduler( + Root, + props, + (scheduler_tx, scheduler_rx), + ); + + Self { + virtual_dom, + edit_queue, + command_rx, + core_cmd_type: PhantomData, + } + } + + pub async fn run(&mut self) { + // apply initial edit + let initial_muts = self.virtual_dom.rebuild(); + self.edit_queue + .lock() + .unwrap() + .push(serde_json::to_string(&initial_muts.edits).unwrap()); + self.rerender(); + + loop { + // wait for either + select! { + // 1) when no work + () = self.virtual_dom.wait_for_work() => { + log::debug!("pulling work"); + self.apply_edits(); + } + // 2) when global state is changed or injected window.document event is emitted + cmd = self.command_rx.receive() => { + if let Some(cmd) = cmd { + match cmd { + VirtualDomCommand::UpdateDom => { + log::debug!("VirtualDomCommand::UpdateDom"); + self.apply_edits(); + } + VirtualDomCommand::GlobalState(state) => { + log::debug!("VirtualDomCommand::GlobalState"); + let root = self.atom_root(); + state.handler(root.clone()); + + self.apply_edits(); + } + }; + } + } + } + + if !self.edit_queue.lock().unwrap().is_empty() { + self.rerender(); + } + } + } + + pub fn provide_ui_context(&self, context: UiContext) + where + CoreCommand: Clone + Debug, + { + self.virtual_dom.base_scope().provide_context(context); + } + + fn atom_root(&self) -> Rc { + let cx = self.virtual_dom.base_scope(); + match cx.consume_context::>() { + Some(root) => root, + None => cx.provide_root_context(Rc::new(AtomRoot::new(cx.schedule_update_any()))), + } + } + + fn apply_edits(&mut self) { + let muts = self.virtual_dom.work_with_deadline(|| false); + for edit in muts { + self.edit_queue + .lock() + .unwrap() + .push(serde_json::to_string(&edit.edits).unwrap()); + } + } + + fn rerender(&self) { + 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 96e2d81..1f829e9 100644 --- a/packages/desktop/src/window.rs +++ b/packages/desktop/src/window.rs @@ -343,17 +343,21 @@ impl DioxusWindows { .map(|message| match message.method() { "user_event" => { let event = trigger_from_serialized(message.params()); + log::debug!("IpcMessage user_event: {event:?}"); dom_tx.unbounded_send(SchedulerMsg::Event(event)).unwrap(); } "keyboard_event" => { + log::debug!("IpcMessage: keyboard_event"); let event = KeyboardEvent::from_value(message.params()); proxy.send_event(UiEvent::KeyboardEvent(event)).unwrap(); } "initialize" => { + log::debug!("IpcMessage: initialize"); is_ready_clone.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = proxy.send_event(UiEvent::WindowEvent(WindowEvent::Update)); + let _ = proxy.send_event(UiEvent::WindowEvent(WindowEvent::Rerender)); } "browser_open" => { + log::debug!("IpcMessage: browser_open"); let data = message.params(); log::trace!("Open browser: {:?}", data); if let Some(temp) = data.as_object() { @@ -490,7 +494,8 @@ impl Window { &self.webview.window() } - pub fn update(&mut self) { + pub fn rerender(&mut self) { + log::debug!("rerender: webview.evaluate_script()"); if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { let mut queue = self.edit_queue.lock().unwrap(); diff --git a/packages/macro/src/lib.rs b/packages/macro/src/lib.rs index 2e4be42..4502865 100644 --- a/packages/macro/src/lib.rs +++ b/packages/macro/src/lib.rs @@ -18,7 +18,8 @@ pub fn global_state(_attr: TokenStream, input: TokenStream) -> TokenStream { } = GlobalStateParser::from(input).parse(); let gen = quote! { - use bevy_dioxus::desktop::event::VDomCommand; + use bevy::{app::Plugin, ecs::system::Res, log::error}; + use bevy_dioxus::desktop::event::VirtualDomCommand; use dioxus::fermi::{Atom, AtomRoot, Readable}; use futures_intrusive::channel::{shared::Sender, TrySendError}; use std::rc::Rc; @@ -49,10 +50,10 @@ pub fn global_state(_attr: TokenStream, input: TokenStream) -> TokenStream { fn apply_global_state_command( mut events: EventReader, - vdom_tx: Res>>, + vdom_tx: Res>>, ) { for e in events.iter() { - match vdom_tx.try_send(VDomCommand::GlobalState(e.clone())) { + match vdom_tx.try_send(VirtualDomCommand::GlobalState(e.clone())) { Ok(()) => {} Err(e) => match e { TrySendError::Full(e) => { diff --git a/packages/website/content/docs/architecture/_index.md b/packages/website/content/docs/architecture/_index.md new file mode 100644 index 0000000..38bba86 --- /dev/null +++ b/packages/website/content/docs/architecture/_index.md @@ -0,0 +1,7 @@ ++++ +title = "Architecture" +redirect_to = "docs/architecture/overview" +weight = 3 +sort_by ="weight" +draft = true ++++ diff --git a/packages/website/content/docs/architecture/overview.md b/packages/website/content/docs/architecture/overview.md new file mode 100644 index 0000000..5c86b11 --- /dev/null +++ b/packages/website/content/docs/architecture/overview.md @@ -0,0 +1,92 @@ ++++ +title = "Overview" +weight = 0 ++++ + +## Channels +```rust +let (vdom_scheduler_tx, vdom_scheduler_rx) = mpsc::unbounded::(); +let (vdom_command_tx, vdom_command_rx) = channel::>(8); +let (core_tx, core_rx) = channel::(8); +let proxy = event_loop.create_proxy(); +``` + +```mermaid +sequenceDiagram + participant WebView + participant Window + participant VirtualDom + participant EventLoop + participant Systems + + WebView ->> Window: IpcMessage + Window ->> VirtualDom: vdom_scheduler_tx.send() + VirtualDom ->> EventLoop: proxy.send() + EventLoop ->> Systems: app.update() + + Systems ->> EventLoop: global_state.send() + EventLoop ->> VirtualDom: vdom_command_tx.send() + VirtualDom ->> Window: dioxus_window.rerender() + Window ->> WebView: webviwe.evaluate_script() +``` + +## Render cycle + +```mermaid +sequenceDiagram + participant VirtualDom + participant EventLoop + participant Systems + + VirtualDom ->> VirtualDom: Wait For Work + Note left of VirtualDom: VirtualDomCommand + Note left of VirtualDom: rerender(); + VirtualDom ->> EventLoop: Event::UserEvent(WindowEvent::Rerender) + EventLoop ->> EventLoop: NewEvents(Init) + EventLoop ->> EventLoop: MainEventsCleared + EventLoop ->> Systems: app.update() + + EventLoop ->> EventLoop: RedrawRequested + EventLoop ->> EventLoop: RedrawEventsCleared +``` + +### When user clicks screen + +```mermaid +sequenceDiagram + participant Window + participant EventLoop + participant Systems + + Note left of Window: User Click + Window ->> EventLoop: Event::DeviceEvent + Note left of EventLoop: User Click + EventLoop ->> Systems: app.update() +``` + +### CoreCommand + +```mermaid +sequenceDiagram + participant WebView + participant Window + participant Plugin + participant EventLoop + participant Systems + participant VirtualDom + + Window ->> Plugin: window.send(cmd) + Plugin ->> EventLoop: proxy.send_event(UiEvent::CoreCommand(cmd)); + EventLoop ->> EventLoop: MainEventsCleared + EventLoop ->> Systems: app.update() + Note right of Systems: apply_globao_state_command + Systems ->> VirtualDom: virtual_dom_command.try_send(VirtualDomCommand::GlobalState(state)); + Note right of VirtualDom: apply_edits() + Note right of VirtualDom: rerender() + VirtualDom ->> EventLoop: Event::UserEvent(WindowEvent::Rerender) + EventLoop ->> Window: dioxus_window.rerender() + Window ->> WebView: webviwe.evaluate_script() + + EventLoop ->> EventLoop: RedrawRequested + EventLoop ->> EventLoop: RedrawEventsCleared +``` diff --git a/packages/website/content/docs/debug/_index.md b/packages/website/content/docs/debug/_index.md index b392d3b..63b0a8c 100644 --- a/packages/website/content/docs/debug/_index.md +++ b/packages/website/content/docs/debug/_index.md @@ -1,7 +1,7 @@ +++ title = "Debug" redirect_to = "docs/debug/overview" -weight = 4 +weight = 5 sort_by ="weight" draft = true +++ diff --git a/packages/website/content/docs/development/_index.md b/packages/website/content/docs/development/_index.md index 606842a..e51f873 100644 --- a/packages/website/content/docs/development/_index.md +++ b/packages/website/content/docs/development/_index.md @@ -1,7 +1,7 @@ +++ title = "Development" redirect_to = "docs/development/overview" -weight = 3 +weight = 4 sort_by ="weight" draft = true +++