1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! Dioxus WebSys
//!
//! ## Overview
//! ------------
//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using WebSys. This web render for
//! Dioxus is one of the more advanced renderers, supporting:
//! - idle work
//! - animations
//! - jank-free rendering
//! - noderefs
//! - controlled components
//! - re-hydration
//! - and more.
//!
//! The actual implementation is farily thin, with the heavy lifting happening inside the Dioxus Core crate.
//!
//! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide
//! validation of websys-specific features and not the general use of Dioxus.
//!
//! ## RequestAnimationFrame and RequestIdleCallback
//! ------------------------------------------------
//! React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long
//! running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the
//! main thread.
//!
//! React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus,
//! the diff phase is non-blocking, using "yield_now" to allow the browser to process other events. When the diff phase
//! is  finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply.
//!
//! Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then
//! setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame
//!
//!     From Google's guide on rAF and rIC:
//!     -----------------------------------
//!
//!     If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
//!     which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
//!      of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
//!      frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
//!      which is a potential performance bottleneck.
//!
//!     Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
//!     and as such we could easily go past the deadline the browser provided.
//!
//!     The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
//!     browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
//!     be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
//!     to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
//!
//!     Essentially:
//!     ------------
//!     - Do the VDOM work during the idlecallback
//!     - Do DOM work in the next requestAnimationFrame callback

use std::rc::Rc;

pub use crate::cfg::WebConfig;
use crate::dom::load_document;
use dioxus::SchedulerMsg;
use dioxus::VirtualDom;
pub use dioxus_core as dioxus;
use dioxus_core::prelude::Component;
use futures_util::FutureExt;

mod cache;
mod cfg;
mod dom;
mod nodeslab;
mod ric_raf;

/// Launch the VirtualDOM given a root component and a configuration.
///
/// This function expects the root component to not have root props. To launch the root component with root props, use
/// `launch_with_props` instead.
///
/// This method will block the thread with `spawn_local` from wasm_bindgen_futures.
///
/// If you need to run the VirtualDOM in its own thread, use `run_with_props` instead and await the future.
///
/// # Example
///
/// ```rust, ignore
/// fn main() {
///     dioxus_web::launch(App);
/// }
///
/// static App: Component = |cx| {
///     rsx!(cx, div {"hello world"})
/// }
/// ```
pub fn launch(root_component: Component) {
    launch_with_props(root_component, (), |c| c);
}

/// Launches the VirtualDOM from the specified component function and props.
///
/// This method will block the thread with `spawn_local`
///
/// # Example
///
/// ```rust, ignore
/// fn main() {
///     dioxus_web::launch_with_props(App, RootProps { name: String::from("joe") });
/// }
///
/// #[derive(ParitalEq, Props)]
/// struct RootProps {
///     name: String
/// }
///
/// static App: Component<RootProps> = |cx| {
///     rsx!(cx, div {"hello {cx.props.name}"})
/// }
/// ```
pub fn launch_with_props<T, F>(
    root_component: Component<T>,
    root_properties: T,
    configuration_builder: F,
) where
    T: Send + 'static,
    F: FnOnce(WebConfig) -> WebConfig,
{
    let config = configuration_builder(WebConfig::default());
    wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config));
}

/// Runs the app as a future that can be scheduled around the main thread.
///
/// Polls futures internal to the VirtualDOM, hence the async nature of this function.
///
/// # Example
///
/// ```ignore
/// fn main() {
///     let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("joe") });
///     wasm_bindgen_futures::spawn_local(app_fut);
/// }
/// ```
pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T, cfg: WebConfig) {
    let mut dom = VirtualDom::new_with_props(root, root_props);

    for s in crate::cache::BUILTIN_INTERNED_STRINGS {
        wasm_bindgen::intern(s);
    }
    for s in &cfg.cached_strings {
        wasm_bindgen::intern(s);
    }

    let should_hydrate = cfg.hydrate;

    let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap();

    let tasks = dom.get_scheduler_channel();

    let sender_callback: Rc<dyn Fn(SchedulerMsg)> =
        Rc::new(move |event| tasks.unbounded_send(event).unwrap());

    let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback);

    let mut mutations = dom.rebuild();

    // hydrating is simply running the dom for a single render. If the page is already written, then the corresponding
    // ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory
    if !should_hydrate {
        websys_dom.process_edits(&mut mutations.edits);
    }

    let work_loop = ric_raf::RafLoop::new();

    loop {
        // if virtualdom has nothing, wait for it to have something before requesting idle time
        // if there is work then this future resolves immediately.
        dom.wait_for_work().await;

        // wait for the mainthread to schedule us in
        let mut deadline = work_loop.wait_for_idle_time().await;

        // run the virtualdom work phase until the frame deadline is reached
        let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some());

        // wait for the animation frame to fire so we can apply our changes
        work_loop.wait_for_raf().await;

        for mut edit in mutations {
            // actually apply our changes during the animation frame
            websys_dom.process_edits(&mut edit.edits);
        }
    }
}