diff --git a/.vscode/settings.json b/.vscode/settings.json index a3b8a0b79..b3bde867c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false, "rust-analyzer.cargo.allFeatures": true } diff --git a/Cargo.toml b/Cargo.toml index 826238085..c088139fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,15 +41,17 @@ futures-util = "0.3.16" log = "0.4.14" num-format = "0.4.0" separator = "0.4.1" -serde = { version = "1.0.126", features = ["derive"] } +serde = { version = "1.0.130", features = ["derive"] } im-rc = "15.0.0" fxhash = "0.2.1" anyhow = "1.0.42" +reqwest = "0.11.4" +serde_json = "1.0.68" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] argh = "0.1.5" env_logger = "0.9.0" -async-std = { version = "1.9.0", features = ["attributes"] } +tokio = { version = "1.12.0", features = ["full"] } rand = { version = "0.8.4", features = ["small_rng"] } gloo-timers = "0.2.1" # surf = { version = "2.3.1", default-features = false, features = [ diff --git a/examples/async.rs b/examples/async.rs index da7649776..91ccbc4e3 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -6,8 +6,9 @@ the `TaskHandle` object. use dioxus::prelude::*; use gloo_timers::future::TimeoutFuture; -fn main() { - dioxus::desktop::launch(App, |c| c).unwrap(); +#[tokio::main] +async fn main() { + dioxus::desktop::launch(App, |c| c).await; } pub static App: FC<()> = |cx, _| { diff --git a/examples/showcase.rs b/examples/showcase.rs deleted file mode 100644 index ca3cc469c..000000000 --- a/examples/showcase.rs +++ /dev/null @@ -1,208 +0,0 @@ -#![allow( - unused, - dead_code, - non_upper_case_globals, - non_camel_case_types, - non_snake_case -)] - -fn main() {} - -use dioxus::prelude::*; -use std::rc::Rc; - -static App: FC<()> = |cx, props| { - let (selection, set_selection) = use_state(cx, || None as Option).classic(); - - let body = match selection { - Some(id) => rsx!(cx, ReferenceItem { selected: *id }), - None => rsx!(cx, div { "Select an concept to explore" }), - }; - - cx.render(rsx! { - div { - ScrollSelector { onselect: move |id| set_selection(id) } - {body} - } - }) -}; - -// this is its own component to stay memoized -#[derive(Props)] -struct ScrollSelectorProps<'a> { - onselect: &'a dyn Fn(Option), -} - -fn ScrollSelector<'a>(cx: Context<'a>, props: &'a ScrollSelectorProps) -> DomTree<'a> { - let selection_list = (&REFERENCES).iter().enumerate().map(|(id, _)| { - rsx! { - li { - h3 {} - } - } - }); - cx.render(rsx! { - div { - h1 {""} - ul { - {selection_list} - button { - onclick: move |_| (props.onselect)(Some(10)) - } - } - } - }) -} - -#[derive(PartialEq, Props)] -struct ReferenceItemProps { - selected: usize, -} - -static ReferenceItem: FC = |cx, props| { - let (caller, name, code) = REFERENCES[props.selected]; - - // Create the component using the factory API directly - let caller_node = LazyNodes::new(move |f| f.component(caller, (), None, &[])); - - cx.render(rsx! { - div { - // Source of the left, rendered on the right - div { - code { "{code}" } - } - div { - {caller_node} - } - } - }) -}; - -use reference::REFERENCES; -mod reference { - mod antipatterns; - mod basics; - mod children; - mod conditional_rendering; - mod controlled_inputs; - mod custom_elements; - mod empty; - mod fragments; - mod global_css; - mod inline_styles; - mod iterators; - mod listener; - mod memo; - mod noderefs; - mod signals; - mod spreadpattern; - mod statemanagement; - mod suspense; - mod task; - mod testing; - mod tostring; - use super::*; - pub static REFERENCES: &[(FC<()>, &str, &str)] = &[ - ( - basics::Example, - "Basics", - include_str!("./reference/basics.rs"), - ), - ( - children::Example, - "Children", - include_str!("./reference/children.rs"), - ), - ( - conditional_rendering::Example, - "Conditional Rendering", - include_str!("./reference/conditional_rendering.rs"), - ), - // TODO - ( - controlled_inputs::Example, - "Controlled Inputs", - include_str!("./reference/controlled_inputs.rs"), - ), - ( - empty::Example, - "empty", - include_str!("./reference/empty.rs"), - ), - ( - custom_elements::Example, - "Custom Elements & Web Components", - include_str!("./reference/custom_elements.rs"), - ), - ( - fragments::Example, - "Fragments", - include_str!("./reference/fragments.rs"), - ), - ( - iterators::Example, - "Iterators", - include_str!("./reference/iterators.rs"), - ), - ( - global_css::Example, - "Global CSS", - include_str!("./reference/global_css.rs"), - ), - ( - inline_styles::Example, - "Inline Styles", - include_str!("./reference/inline_styles.rs"), - ), - ( - listener::Example, - "Listener", - include_str!("./reference/listener.rs"), - ), - (memo::Example, "Memo", include_str!("./reference/memo.rs")), - ( - spreadpattern::Example, - "Spread Pattern", - include_str!("./reference/spreadpattern.rs"), - ), - ( - suspense::Example, - "Suspense", - include_str!("./reference/suspense.rs"), - ), - (task::Example, "Task", include_str!("./reference/task.rs")), - ( - tostring::Example, - "Tostring", - include_str!("./reference/tostring.rs"), - ), - ( - antipatterns::Example, - "Anti-patterns", - include_str!("./reference/antipatterns.rs"), - ), - /* - TODO! these showcase features that aren't quite ready - */ - ( - signals::Example, - "Signals", - include_str!("./reference/signals.rs"), - ), - ( - noderefs::Example, - "NodeRefs", - include_str!("./reference/noderefs.rs"), - ), - ( - statemanagement::Example, - "State Management", - include_str!("./reference/statemanagement.rs"), - ), - ( - testing::Example, - "Testing", - include_str!("./reference/testing.rs"), - ), - ]; -} diff --git a/examples/weather_app.rs b/examples/weather_app.rs index 833cc0529..8ebf69448 100644 --- a/examples/weather_app.rs +++ b/examples/weather_app.rs @@ -7,15 +7,22 @@ use dioxus::prelude::*; fn main() { - // dioxus::desktop::launch(App, |c| c); + dioxus::desktop::launch(App, |c| c); } +const ENDPOINT: &str = "https://api.openweathermap.org/data/2.5/weather"; + static App: FC<()> = |cx, props| { // let body = use_suspense( cx, || async { - // + let content = reqwest::get(ENDPOINT) + .await + .unwrap() + .json::() + .await + .unwrap(); }, |cx, props| { // diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 9d16c9c01..b3df1af3f 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -1,4 +1,8 @@ //! Public APIs for managing component state, tasks, and lifecycles. +//! +//! This module is separate from `Scope` to narrow what exactly is exposed to user code. +//! +//! We unsafely implement `send` for the VirtualDom, but those guarantees can only be use crate::innerlude::*; use std::{any::TypeId, ops::Deref, rc::Rc}; diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 7b01a26a9..88746faf3 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -14,6 +14,7 @@ use std::{ fmt::Debug, ops::Deref, rc::Rc, + sync::Arc, }; #[derive(Debug)] @@ -216,13 +217,13 @@ pub mod on { )* ) => { $( $(#[$attr])* - pub struct $wrapper(pub Rc); + pub struct $wrapper(pub Arc); // todo: derefing to the event is fine (and easy) but breaks some IDE stuff like (go to source) // going to source in fact takes you to the source of Rc which is... less than useful // Either we ask for this to be changed in Rust-analyzer or manually impkement the trait impl Deref for $wrapper { - type Target = Rc; + type Target = Arc; fn deref(&self) -> &Self::Target { &self.0 } @@ -598,7 +599,7 @@ pub mod on { ]; } - pub struct GenericEvent(pub Rc); + pub struct GenericEvent(pub Arc); pub trait GenericEventInner { /// Return a reference to the raw event. User will need to downcast the event to the right platform-specific type. diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 6404ebe73..047366129 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -29,6 +29,7 @@ pub mod scheduler; pub mod scope; pub mod tasks; pub mod test_dom; +pub mod threadsafe; pub mod util; pub mod virtual_dom; @@ -50,6 +51,7 @@ pub(crate) mod innerlude { pub use crate::scope::*; pub use crate::tasks::*; pub use crate::test_dom::*; + pub use crate::threadsafe::*; pub use crate::util::*; pub use crate::virtual_dom::*; @@ -60,7 +62,7 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, SuspendedContext, SyntheticEvent, TaskHandle, - TestDom, UserEvent, VNode, VirtualDom, FC, + TestDom, ThreadsafeVirtualDom, UserEvent, VNode, VirtualDom, FC, }; pub mod prelude { diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 19006042e..74f7db2c6 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -7,6 +7,7 @@ use std::{ future::Future, pin::Pin, rc::Rc, + sync::Arc, }; /// Every component in Dioxus is represented by a `Scope`. diff --git a/packages/core/src/threadsafe.rs b/packages/core/src/threadsafe.rs new file mode 100644 index 000000000..042c21f2a --- /dev/null +++ b/packages/core/src/threadsafe.rs @@ -0,0 +1,58 @@ +//! A threadsafe wrapper for the VirtualDom + +use std::sync::{Arc, Mutex, MutexGuard}; + +use crate::VirtualDom; + +/// A threadsafe wrapper for the Dioxus VirtualDom. +/// +/// The Dioxus VirtualDom is not normally `Send` because user code can contain non-`Send` types. However, it is important +/// to have a VirtualDom that is `Send` when used in server-side code since very few web frameworks support non-send +/// handlers. +/// +/// To address this, we have the `ThreadsafeVirtualDom` type which is a threadsafe wrapper for the VirtualDom. To access +/// the VirtualDom, it must be first unlocked using the `lock` method. This locks the VirtualDom through a mutex and +/// prevents any user code from leaking out. It is not possible to acquire any non-`Send` types from inside the VirtualDom. +/// +/// The only way data may be accessed through the VirtualDom is from the "root props" method or by accessing a `Scope` +/// directly. Even then, it's not possible to access any hook data. This means that non-Send types are only "in play" +/// while the VirtualDom is locked with a non-Send marker. +/// +/// Note that calling "wait for work" on the regular VirtualDom is inherently non-Send. If there are async tasks that +/// need to be awaited, they must be done thread-local since we don't place any requirements on user tasks. This can be +/// done with the function "spawn_local" in either tokio or async_std. +pub struct ThreadsafeVirtualDom { + inner: Arc>, +} + +impl ThreadsafeVirtualDom { + pub fn new(inner: VirtualDom) -> Self { + let inner = Arc::new(Mutex::new(inner)); + Self { inner } + } + + pub fn lock(&self) -> Option { + let locked = self.inner.lock().unwrap(); + Some(VirtualDomGuard { guard: locked }) + } +} + +unsafe impl Send for ThreadsafeVirtualDom {} + +pub struct VirtualDomGuard<'a> { + guard: MutexGuard<'a, VirtualDom>, +} + +impl<'a> std::ops::Deref for VirtualDomGuard<'a> { + type Target = MutexGuard<'a, VirtualDom>; + + fn deref(&self) -> &Self::Target { + &self.guard + } +} + +impl<'a> std::ops::DerefMut for VirtualDomGuard<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.guard + } +} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1c732d862..a9da36b9d 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -20,8 +20,7 @@ //! Additional functionality is defined in the respective files. use crate::innerlude::*; -use futures_util::Future; -use std::any::Any; +use std::{any::Any, rc::Rc, sync::Arc}; /// An integrated virtual node system that progresses events and diffs UI trees. /// @@ -60,10 +59,10 @@ pub struct VirtualDom { root_fc: Box, - root_props: Box, + root_props: Rc, // we need to keep the allocation around, but we don't necessarily use it - _root_caller: Box Fn(&'b Scope) -> DomTree<'b> + 'static>, + _root_caller: RootCaller, } impl VirtualDom { @@ -121,16 +120,15 @@ impl VirtualDom { /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" }); /// let mutations = dom.rebuild(); /// ``` - pub fn new_with_props(root: FC

, root_props: P) -> Self { + pub fn new_with_props(root: FC

, root_props: P) -> Self { let root_fc = Box::new(root); - let root_props: Box = Box::new(root_props); - - let props_ptr = root_props.downcast_ref::

().unwrap() as *const P; + let root_props: Rc = Rc::new(root_props); + let _p = root_props.clone(); // Safety: this callback is only valid for the lifetime of the root props - let root_caller: Box DomTree> = Box::new(move |scope: &Scope| unsafe { - let props: &'_ P = &*(props_ptr as *const P); + let root_caller: Rc DomTree> = Rc::new(move |scope: &Scope| unsafe { + let props = _p.downcast_ref::

().unwrap(); std::mem::transmute(root(Context { scope }, props)) }); @@ -148,7 +146,7 @@ impl VirtualDom { }); Self { - _root_caller: root_caller, + _root_caller: RootCaller(root_caller), root_fc, base_scope, scheduler, @@ -189,14 +187,14 @@ impl VirtualDom { /// ``` pub fn update_root_props

(&mut self, root_props: P) -> Option where - P: 'static, + P: 'static + Send, { let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap(); // Pre-emptively drop any downstream references of the old props root_scope.ensure_drop_safety(&self.scheduler.pool); - let mut root_props: Box = Box::new(root_props); + let mut root_props: Rc = Rc::new(root_props); if let Some(props_ptr) = root_props.downcast_ref::

().map(|p| p as *const P) { // Swap the old props and new props @@ -345,46 +343,47 @@ impl VirtualDom { /// Waits for the scheduler to have work /// This lets us poll async tasks during idle periods without blocking the main thread. pub async fn wait_for_work(&mut self) { - if self.scheduler.has_any_work() { - log::debug!("No need to wait for work, we already have some"); - return; - } + todo!("making vdom send right now"); + // if self.scheduler.has_any_work() { + // log::debug!("No need to wait for work, we already have some"); + // return; + // } - log::debug!("No active work.... waiting for some..."); - use futures_util::StreamExt; + // log::debug!("No active work.... waiting for some..."); + // use futures_util::StreamExt; - // right now this won't poll events if there is ongoing work - // in the future we want to prioritize some events over ongoing work - // this is coming in the "priorities" PR + // // right now this won't poll events if there is ongoing work + // // in the future we want to prioritize some events over ongoing work + // // this is coming in the "priorities" PR - // Wait for any new events if we have nothing to do - // todo: poll the events once even if there is work to do to prevent starvation - futures_util::select! { - _ = self.scheduler.async_tasks.next() => {} - msg = self.scheduler.receiver.next() => { - match msg.unwrap() { - SchedulerMsg::Task(t) => todo!(), - SchedulerMsg::Immediate(im) => { - self.scheduler.dirty_scopes.insert(im); - } - SchedulerMsg::UiEvent(evt) => { - self.scheduler.ui_events.push_back(evt); - } - } - }, - } + // // Wait for any new events if we have nothing to do + // // todo: poll the events once even if there is work to do to prevent starvation + // futures_util::select! { + // _ = self.scheduler.async_tasks.next() => {} + // msg = self.scheduler.receiver.next() => { + // match msg.unwrap() { + // SchedulerMsg::Task(t) => todo!(), + // SchedulerMsg::Immediate(im) => { + // self.scheduler.dirty_scopes.insert(im); + // } + // SchedulerMsg::UiEvent(evt) => { + // self.scheduler.ui_events.push_back(evt); + // } + // } + // }, + // } - while let Ok(Some(msg)) = self.scheduler.receiver.try_next() { - match msg { - SchedulerMsg::Task(t) => todo!(), - SchedulerMsg::Immediate(im) => { - self.scheduler.dirty_scopes.insert(im); - } - SchedulerMsg::UiEvent(evt) => { - self.scheduler.ui_events.push_back(evt); - } - } - } + // while let Ok(Some(msg)) = self.scheduler.receiver.try_next() { + // match msg { + // SchedulerMsg::Task(t) => todo!(), + // SchedulerMsg::Immediate(im) => { + // self.scheduler.dirty_scopes.insert(im); + // } + // SchedulerMsg::UiEvent(evt) => { + // self.scheduler.ui_events.push_back(evt); + // } + // } + // } } } @@ -407,3 +406,15 @@ impl std::fmt::Display for VirtualDom { renderer.render(self, root, f, 0) } } + +/* +Send safety... + +The VirtualDom can only be "send" if the internals exposed to user code are also "send". + +IE it's okay to move an Rc from one thread to another thread as long as it's only used internally + +*/ + +// we never actually use the contents of this root caller +struct RootCaller(Rc Fn(&'b Scope) -> DomTree<'b> + 'static>); diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs index 537a2ef86..aadb6ebda 100644 --- a/packages/core/tests/create_dom.rs +++ b/packages/core/tests/create_dom.rs @@ -11,7 +11,7 @@ use dioxus_html as dioxus_elements; mod test_logging; use DomEdit::*; -fn new_dom(app: FC

, props: P) -> VirtualDom { +fn new_dom(app: FC

, props: P) -> VirtualDom { const IS_LOGGING_ENABLED: bool = false; test_logging::set_up_logging(IS_LOGGING_ENABLED); VirtualDom::new_with_props(app, props) diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index f22ce246d..b130ea2e8 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -1,33 +1,31 @@ //! Tests for the lifecycle of components. -use std::{cell::RefCell, rc::Rc}; - use anyhow::{Context, Result}; use dioxus::prelude::*; use dioxus_core as dioxus; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; +use std::sync::{Arc, Mutex}; mod test_logging; const IS_LOGGING_ENABLED: bool = true; -type Shared = Rc>; +type Shared = Arc>; #[test] fn manual_diffing() { test_logging::set_up_logging(IS_LOGGING_ENABLED); - #[derive(PartialEq, Props)] struct AppProps { value: Shared<&'static str>, } static App: FC = |cx, props| { - let val = props.value.borrow(); + let val = props.value.lock().unwrap(); cx.render(rsx! { div { "{val}" } }) }; - let value = Rc::new(RefCell::new("Hello")); + let value = Arc::new(Mutex::new("Hello")); let mut dom = VirtualDom::new_with_props( App, AppProps { @@ -37,7 +35,7 @@ fn manual_diffing() { let _ = dom.rebuild(); - *value.borrow_mut() = "goodbye"; + *value.lock().unwrap() = "goodbye"; let edits = dom.diff(); diff --git a/packages/desktop/.vscode/settings.json b/packages/desktop/.vscode/settings.json index 80f51cff8..1c68f97cc 100644 --- a/packages/desktop/.vscode/settings.json +++ b/packages/desktop/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false -} \ No newline at end of file + "rust-analyzer.inlayHints.enable": true +} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index d3e474966..f95d4b0b5 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -20,14 +20,14 @@ log = "0.4.13" fern = { version = "0.6.0", features = ["colored"] } html-escape = "0.2.9" wry = "0.11.0" -async-std = { version = "1.9.0", features = ["attributes"] } +tokio = { version = "1.12.0", features = ["full"] } [dev-dependencies] dioxus-html = { path = "../html" } tide = "0.15.0" tide-websockets = "0.3.0" - +dioxus-core-macro = { path = "../core-macro" } # thiserror = "1.0.23" # log = "0.4.13" # fern = { version = "0.6.0", features = ["colored"] } diff --git a/packages/desktop/examples/demo.rs b/packages/desktop/examples/demo.rs new file mode 100644 index 000000000..bde43c18c --- /dev/null +++ b/packages/desktop/examples/demo.rs @@ -0,0 +1,24 @@ +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_core_macro::*; + +use dioxus_html as dioxus_elements; + +fn main() { + std::thread::spawn(|| { + let mut vdom = VirtualDom::new(App); + let f = async_std::task::block_on(vdom.wait_for_work()); + }); + let a = 10; + // async_std::task::spawn_blocking(|| async move { + // }); +} + +static App: FC<()> = |cx, props| { + // + cx.render(rsx!( + div { + "hello world!" + } + )) +}; diff --git a/packages/desktop/examples/test.rs b/packages/desktop/examples/test.rs deleted file mode 100644 index b327b1c33..000000000 --- a/packages/desktop/examples/test.rs +++ /dev/null @@ -1,16 +0,0 @@ -use dioxus_core as dioxus; -use dioxus_core::prelude::*; -use dioxus_html as dioxus_elements; - -fn main() { - dioxus_desktop::launch(App, |f| f.with_window(|w| w.with_maximized(true))).expect("Failed"); -} - -static App: FC<()> = |cx, props|{ - // - cx.render(rsx!( - div { - "hello world!" - } - )) -}; diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 83f2a8bba..8ff2a9996 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -2,6 +2,7 @@ //! use std::rc::Rc; +use std::sync::Arc; use dioxus_core::{ events::{ @@ -21,7 +22,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { let mut data: Vec = serde_json::from_value(val).unwrap(); let data = data.drain(..).next().unwrap(); - let event = SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent))); + let event = SyntheticEvent::MouseEvent(MouseEvent(Arc::new(WebviewMouseEvent))); let scope = ScopeId(data.scope as usize); let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize)); UserEvent { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 50528288a..6b9ba9ea0 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -1,5 +1,11 @@ +//! Dioxus Desktop Renderer +//! +//! Render the Dioxus VirtualDom using the platform's native WebView implementation. +//! + use std::borrow::BorrowMut; use std::ops::{Deref, DerefMut}; +use std::sync::atomic::AtomicBool; use std::sync::mpsc::channel; use std::sync::{Arc, RwLock}; @@ -21,7 +27,6 @@ mod cfg; mod dom; mod escape; mod events; -use events::*; static HTML_CONTENT: &'static str = include_str!("./index.html"); @@ -31,20 +36,15 @@ pub fn launch( ) -> anyhow::Result<()> { launch_with_props(root, (), config_builder) } -pub fn launch_with_props( + +pub fn launch_with_props( root: FC

, props: P, builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>, ) -> anyhow::Result<()> { - WebviewRenderer::run(root, props, builder) + run(root, props, builder) } -/// The `WebviewRenderer` provides a way of rendering a Dioxus Virtual DOM through a bridge to a Webview instance. -/// Components used in WebviewRenderer instances can directly use system libraries, access the filesystem, and multithread with ease. -pub struct WebviewRenderer { - /// The root component used to render the Webview - root: FC, -} enum RpcEvent<'a> { Initialize { edits: Vec> }, } @@ -55,184 +55,165 @@ struct Response<'a> { edits: Vec>, } -impl WebviewRenderer { - pub fn run( - root: FC, - props: T, - user_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>, - ) -> anyhow::Result<()> { - Self::run_with_edits(root, props, user_builder, None) - } - - pub fn run_with_edits< - F: for<'a, 'b> FnOnce(&'a mut DesktopConfig<'b>) -> &'a mut DesktopConfig<'b>, - >( - root: FC, - props: T, - user_builder: F, - redits: Option>>, - ) -> anyhow::Result<()> { - log::info!("hello edits"); - let event_loop = EventLoop::new(); - - let mut cfg = DesktopConfig::new(); - user_builder(&mut cfg); - - let DesktopConfig { - window, - manual_edits, - pre_rendered, - } = cfg; - - let window = window.build(&event_loop)?; - - let mut vir = VirtualDom::new_with_props(root, props); - - let channel = vir.get_event_sender(); - struct WebviewBridge {} - // impl RealDom for WebviewBridge { - // fn raw_node_as_any(&self) -> &mut dyn std::any::Any { - // todo!() - // } - - // fn must_commit(&self) -> bool { - // false - // } - - // fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} - - // fn wait_until_ready<'s>( - // &'s mut self, - // ) -> std::pin::Pin + 's>> { - // // - // Box::pin(async { - // // - // }) - // } - // } - - let mut real_dom = WebviewBridge {}; - // async_std::task::spawn_local(vir.run(&mut real_dom)); - - // todo: combine these or something - let vdom = Arc::new(RwLock::new(vir)); - // let registry = Arc::new(RwLock::new(Some(WebviewRegistry::new()))); - - let webview = WebViewBuilder::new(window)? - .with_url(&format!("data:text/html,{}", HTML_CONTENT))? - // .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| { - // match req.method.as_str() { - // "initiate" => { - // let edits = if let Some(edits) = &redits { - // serde_json::to_value(edits).unwrap() - // } else { - // let mut lock = vdom.write().unwrap(); - // // let mut reg_lock = registry.write().unwrap(); - // // Create the thin wrapper around the registry to collect the edits into - // let mut real = dom::WebviewDom::new(); - // let pre = pre_rendered.clone(); - // let response = match pre { - // Some(content) => { - // lock.rebuild_in_place().unwrap(); - // Response { - // edits: Vec::new(), - // pre_rendered: Some(content), - // } - // } - // None => { - // // - // let edits = { - // // let mut edits = Vec::new(); - // todo!() - // // lock.rebuild(&mut real, &mut edits).unwrap(); - // // edits - // }; - // Response { - // edits, - // pre_rendered: None, - // } - // } - // }; - // serde_json::to_value(&response).unwrap() - // }; - // // Return the edits into the webview runtime - // Some(RpcResponse::new_result(req.id.take(), Some(edits))) - // } - // "user_event" => { - // log::debug!("User event received"); - // // let registry = registry.clone(); - // let vdom = vdom.clone(); - // let response = async_std::task::block_on(async move { - // let mut lock = vdom.write().unwrap(); - // // let mut reg_lock = registry.write().unwrap(); - // // a deserialized event - // let data = req.params.unwrap(); - // log::debug!("Data: {:#?}", data); - // let event = trigger_from_serialized(data); - // // lock.queue_event(event); - // // Create the thin wrapper around the registry to collect the edits into - // let mut real = dom::WebviewDom::new(); - // // Serialize the edit stream - // // - // let mut edits = Vec::new(); - // // lock.run(&mut real, &mut edits) - // // .await - // // .expect("failed to progress"); - // let response = Response { - // edits, - // pre_rendered: None, - // }; - // let response = serde_json::to_value(&response).unwrap(); - // // Give back the registry into its slot - // // *reg_lock = Some(real.consume()); - // // Return the edits into the webview runtime - // Some(RpcResponse::new_result(req.id.take(), Some(response))) - // }); - // response - // // spawn a task to clean up the garbage - // } - // _ => todo!("this message failed"), - // } - // }) - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::WindowEvent { - event, window_id, .. - } => match event { - WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::Resized(_) | WindowEvent::Moved(_) => { - let _ = webview.resize(); - } - _ => {} - }, - - Event::MainEventsCleared => { - webview.resize(); - // window.request_redraw(); - } - - _ => {} - } - }); - } - - /// Create a new text renderer from an existing Virtual DOM. - /// This will progress the existing VDom's events to completion. - pub fn from_vdom() -> Self { - todo!() - } - - /// Pass new args to the root function - pub fn update(&mut self, _new_val: T) { - todo!() - } - - /// Modify the root function in place, forcing a re-render regardless if the props changed - pub fn update_mut(&mut self, _modifier: impl Fn(&mut T)) { - todo!() - } +pub fn run( + root: FC, + props: T, + user_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>, +) -> anyhow::Result<()> { + run_with_edits(root, props, user_builder, None) } + +pub fn run_with_edits< + F: for<'a, 'b> FnOnce(&'a mut DesktopConfig<'b>) -> &'a mut DesktopConfig<'b>, + T: Properties + 'static + Send + Sync, +>( + root: FC, + props: T, + user_builder: F, + redits: Option>>, +) -> anyhow::Result<()> { + /* + + + */ + + let mut cfg = DesktopConfig::new(); + user_builder(&mut cfg); + let DesktopConfig { + window, + manual_edits, + pre_rendered, + } = cfg; + + let event_loop = EventLoop::new(); + let window = window.build(&event_loop)?; + + let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel::(); + + // Spawn the virtualdom onto its own thread + // if it wants to spawn multithreaded tasks, it can use the executor directly + std::thread::spawn(move || { + // + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + runtime.block_on(async move { + let mut vir = VirtualDom::new_with_props(root, props); + let channel = vir.get_event_sender(); + loop { + vir.wait_for_work().await; + let edits = vir.run_with_deadline(|| false); + let edit_string = serde_json::to_string(&edits[0].edits).unwrap(); + event_tx.send(edit_string).unwrap(); + } + }) + }); + + let dioxus_requsted = Arc::new(AtomicBool::new(false)); + + let webview = WebViewBuilder::new(window)? + .with_url(&format!("data:text/html,{}", HTML_CONTENT))? + .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| { + match req.method.as_str() { + "initiate" => {} + "user_event" => {} + _ => todo!("this message failed"), + } + todo!() + }) + .build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event, window_id, .. + } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::Resized(_) | WindowEvent::Moved(_) => { + let _ = webview.resize(); + } + _ => {} + }, + + Event::MainEventsCleared => { + webview.resize(); + // window.request_redraw(); + } + + _ => {} + } + }); + + Ok(()) +} + +// let edits = if let Some(edits) = &redits { +// serde_json::to_value(edits).unwrap() +// } else { +// let mut lock = vdom.write().unwrap(); +// // let mut reg_lock = registry.write().unwrap(); +// // Create the thin wrapper around the registry to collect the edits into +// let mut real = dom::WebviewDom::new(); +// let pre = pre_rendered.clone(); +// let response = match pre { +// Some(content) => { +// lock.rebuild_in_place().unwrap(); +// Response { +// edits: Vec::new(), +// pre_rendered: Some(content), +// } +// } +// None => { +// // +// let edits = { +// // let mut edits = Vec::new(); +// todo!() +// // lock.rebuild(&mut real, &mut edits).unwrap(); +// // edits +// }; +// Response { +// edits, +// pre_rendered: None, +// } +// } +// }; +// serde_json::to_value(&response).unwrap() +// }; +// // Return the edits into the webview runtime +// Some(RpcResponse::new_result(req.id.take(), Some(edits))) + +// log::debug!("User event received"); +// // let registry = registry.clone(); +// let vdom = vdom.clone(); +// let response = async_std::task::block_on(async move { +// let mut lock = vdom.write().unwrap(); +// // let mut reg_lock = registry.write().unwrap(); +// // a deserialized event +// let data = req.params.unwrap(); +// log::debug!("Data: {:#?}", data); +// let event = trigger_from_serialized(data); +// // lock.queue_event(event); +// // Create the thin wrapper around the registry to collect the edits into +// let mut real = dom::WebviewDom::new(); +// // Serialize the edit stream +// // +// let mut edits = Vec::new(); +// // lock.run(&mut real, &mut edits) +// // .await +// // .expect("failed to progress"); +// let response = Response { +// edits, +// pre_rendered: None, +// }; +// let response = serde_json::to_value(&response).unwrap(); +// // Give back the registry into its slot +// // *reg_lock = Some(real.consume()); +// // Return the edits into the webview runtime +// Some(RpcResponse::new_result(req.id.take(), Some(response))) +// }); +// response +// // spawn a task to clean up the garbage diff --git a/packages/ssr/examples/tide.rs b/packages/ssr/examples/tide.rs index 12b085fe8..d86fbdf55 100644 --- a/packages/ssr/examples/tide.rs +++ b/packages/ssr/examples/tide.rs @@ -8,53 +8,54 @@ use dioxus_core_macro::*; use dioxus_hooks::use_state; use dioxus_html as dioxus_elements; -use tide::{Request, Response}; +fn main() {} +// use tide::{Request, Response}; -#[async_std::main] -async fn main() -> Result<(), std::io::Error> { - let mut app = tide::new(); +// #[async_std::main] +// async fn main() -> Result<(), std::io::Error> { +// let mut app = tide::new(); - app.at("/:name").get(|req: Request<()>| async move { - let initial_name: String = req - .param("name") - .map(|f| f.parse().unwrap_or("...?".to_string())) - .unwrap_or("...?".to_string()); +// app.at("/:name").get(|req: Request<()>| async move { +// let initial_name: String = req +// .param("name") +// .map(|f| f.parse().unwrap_or("...?".to_string())) +// .unwrap_or("...?".to_string()); - let mut dom = VirtualDom::new_with_props(Example, ExampleProps { initial_name }); +// let mut dom = VirtualDom::new_with_props(Example, ExampleProps { initial_name }); - dom.rebuild(); +// dom.rebuild(); - Ok(Response::builder(200) - .body(format!("{}", dioxus_ssr::render_vdom(&dom, |c| c))) - .content_type(tide::http::mime::HTML) - .build()) - }); +// Ok(Response::builder(200) +// .body(format!("{}", dioxus_ssr::render_vdom(&dom, |c| c))) +// .content_type(tide::http::mime::HTML) +// .build()) +// }); - println!("Server available at [http://127.0.0.1:8080/bill]"); - app.listen("127.0.0.1:8080").await?; +// println!("Server available at [http://127.0.0.1:8080/bill]"); +// app.listen("127.0.0.1:8080").await?; - Ok(()) -} +// Ok(()) +// } -#[derive(PartialEq, Props)] -struct ExampleProps { - initial_name: String, -} +// #[derive(PartialEq, Props)] +// struct ExampleProps { +// initial_name: String, +// } -static Example: FC = |cx, props| { - let dispaly_name = use_state(cx, move || props.initial_name.clone()); +// static Example: FC = |cx, props| { +// let dispaly_name = use_state(cx, move || props.initial_name.clone()); - cx.render(rsx! { - div { class: "py-12 px-4 text-center w-full max-w-2xl mx-auto", - span { class: "text-sm font-semibold" - "Dioxus Example: Jack and Jill" - } - h2 { class: "text-5xl mt-2 mb-6 leading-tight font-semibold font-heading" - "Hello, {dispaly_name}" - } - ul { - {(0..10).map(|f| rsx!( li {"Element {f}"} ))} - } - } - }) -}; +// cx.render(rsx! { +// div { class: "py-12 px-4 text-center w-full max-w-2xl mx-auto", +// span { class: "text-sm font-semibold" +// "Dioxus Example: Jack and Jill" +// } +// h2 { class: "text-5xl mt-2 mb-6 leading-tight font-semibold font-heading" +// "Hello, {dispaly_name}" +// } +// ul { +// {(0..10).map(|f| rsx!( li {"Element {f}"} ))} +// } +// } +// }) +// }; diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 438062d06..1aa364ef4 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -14,7 +14,7 @@ use dioxus_core::{ DomEdit, ElementId, ScopeId, }; use fxhash::FxHashMap; -use std::{fmt::Debug, rc::Rc}; +use std::{fmt::Debug, rc::Rc, sync::Arc}; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{ Attr, CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, @@ -471,77 +471,79 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> SyntheticEvent { use dioxus_core::events::on::*; match event.type_().as_str() { "copy" | "cut" | "paste" => { - SyntheticEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent(event)))) + SyntheticEvent::ClipboardEvent(ClipboardEvent(Arc::new(WebsysClipboardEvent(event)))) } "compositionend" | "compositionstart" | "compositionupdate" => { let evt: web_sys::CompositionEvent = event.dyn_into().unwrap(); - SyntheticEvent::CompositionEvent(CompositionEvent(Rc::new(WebsysCompositionEvent(evt)))) + SyntheticEvent::CompositionEvent(CompositionEvent(Arc::new(WebsysCompositionEvent( + evt, + )))) } "keydown" | "keypress" | "keyup" => { let evt: web_sys::KeyboardEvent = event.dyn_into().unwrap(); - SyntheticEvent::KeyboardEvent(KeyboardEvent(Rc::new(WebsysKeyboardEvent(evt)))) + SyntheticEvent::KeyboardEvent(KeyboardEvent(Arc::new(WebsysKeyboardEvent(evt)))) } "focus" | "blur" => { let evt: web_sys::FocusEvent = event.dyn_into().unwrap(); - SyntheticEvent::FocusEvent(FocusEvent(Rc::new(WebsysFocusEvent(evt)))) + SyntheticEvent::FocusEvent(FocusEvent(Arc::new(WebsysFocusEvent(evt)))) } "change" => { let evt = event.dyn_into().unwrap(); - SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::GenericEvent(GenericEvent(Arc::new(WebsysGenericUiEvent(evt)))) } "input" | "invalid" | "reset" | "submit" => { let evt: web_sys::Event = event.dyn_into().unwrap(); - SyntheticEvent::FormEvent(FormEvent(Rc::new(WebsysFormEvent(evt)))) + SyntheticEvent::FormEvent(FormEvent(Arc::new(WebsysFormEvent(evt)))) } "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { let evt: web_sys::MouseEvent = event.dyn_into().unwrap(); - SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebsysMouseEvent(evt)))) + SyntheticEvent::MouseEvent(MouseEvent(Arc::new(WebsysMouseEvent(evt)))) } "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { let evt: web_sys::PointerEvent = event.dyn_into().unwrap(); - SyntheticEvent::PointerEvent(PointerEvent(Rc::new(WebsysPointerEvent(evt)))) + SyntheticEvent::PointerEvent(PointerEvent(Arc::new(WebsysPointerEvent(evt)))) } "select" => { let evt: web_sys::UiEvent = event.dyn_into().unwrap(); - SyntheticEvent::SelectionEvent(SelectionEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::SelectionEvent(SelectionEvent(Arc::new(WebsysGenericUiEvent(evt)))) } "touchcancel" | "touchend" | "touchmove" | "touchstart" => { let evt: web_sys::TouchEvent = event.dyn_into().unwrap(); - SyntheticEvent::TouchEvent(TouchEvent(Rc::new(WebsysTouchEvent(evt)))) + SyntheticEvent::TouchEvent(TouchEvent(Arc::new(WebsysTouchEvent(evt)))) } "scroll" => { let evt: web_sys::UiEvent = event.dyn_into().unwrap(); - SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::GenericEvent(GenericEvent(Arc::new(WebsysGenericUiEvent(evt)))) } "wheel" => { let evt: web_sys::WheelEvent = event.dyn_into().unwrap(); - SyntheticEvent::WheelEvent(WheelEvent(Rc::new(WebsysWheelEvent(evt)))) + SyntheticEvent::WheelEvent(WheelEvent(Arc::new(WebsysWheelEvent(evt)))) } "animationstart" | "animationend" | "animationiteration" => { let evt: web_sys::AnimationEvent = event.dyn_into().unwrap(); - SyntheticEvent::AnimationEvent(AnimationEvent(Rc::new(WebsysAnimationEvent(evt)))) + SyntheticEvent::AnimationEvent(AnimationEvent(Arc::new(WebsysAnimationEvent(evt)))) } "transitionend" => { let evt: web_sys::TransitionEvent = event.dyn_into().unwrap(); - SyntheticEvent::TransitionEvent(TransitionEvent(Rc::new(WebsysTransitionEvent(evt)))) + SyntheticEvent::TransitionEvent(TransitionEvent(Arc::new(WebsysTransitionEvent(evt)))) } "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate" | "volumechange" | "waiting" => { let evt: web_sys::UiEvent = event.dyn_into().unwrap(); - SyntheticEvent::MediaEvent(MediaEvent(Rc::new(WebsysMediaEvent(evt)))) + SyntheticEvent::MediaEvent(MediaEvent(Arc::new(WebsysMediaEvent(evt)))) } "toggle" => { let evt: web_sys::UiEvent = event.dyn_into().unwrap(); - SyntheticEvent::ToggleEvent(ToggleEvent(Rc::new(WebsysToggleEvent(evt)))) + SyntheticEvent::ToggleEvent(ToggleEvent(Arc::new(WebsysToggleEvent(evt)))) } _ => { let evt: web_sys::UiEvent = event.dyn_into().unwrap(); - SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt)))) + SyntheticEvent::GenericEvent(GenericEvent(Arc::new(WebsysGenericUiEvent(evt)))) } } } diff --git a/src/lib.rs b/src/lib.rs index 9f406cceb..11d1b9708 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -193,7 +193,7 @@ pub mod debug {} pub mod prelude { //! A glob import that includes helper types like FC, rsx!, html!, and required traits pub use dioxus_core::prelude::*; - use dioxus_core_macro::{format_args_f, html, rsx, Props}; + pub use dioxus_core_macro::{format_args_f, html, rsx, Props}; pub use dioxus_elements::{GlobalAttributes, SvgAttributes}; pub use dioxus_hooks::*; pub use dioxus_html as dioxus_elements;