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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#![deny(missing_docs)]

//! 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;
pub use crate::util::use_eval;
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 rehydrate;
mod ric_raf;
mod util;

/// 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);
}

/// Launch your app and run the event loop, with configuration.
///
/// This function will start your web app on the main web thread.
///
/// You can configure the WebView window with a configuration closure
///
/// ```rust
/// use dioxus::prelude::*;
///
/// fn main() {
///     dioxus_web::launch_with_props(App, |config| config.pre_render(true));
/// }
///
/// fn app(cx: Scope) -> Element {
///     cx.render(rsx!{
///         h1 {"hello world!"}
///     })
/// }
/// ```
pub fn launch_cfg(root: Component, config_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig) {
    launch_with_props(root, (), config_builder)
}

/// 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") },
///         |config| config
///     );
/// }
///
/// #[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>(
    root_component: Component<T>,
    root_properties: T,
    configuration_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig,
) where
    T: Send + 'static,
{
    if cfg!(feature = "panic_hook") {
        console_error_panic_hook::set_once();
    }

    let mut config = WebConfig::default();
    configuration_builder(&mut config);
    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 tasks = dom.get_scheduler_channel();

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

    let should_hydrate = cfg.hydrate;

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

    log::trace!("rebuilding app");

    if should_hydrate {
        // todo: we need to split rebuild and initialize into two phases
        // it's a waste to produce edits just to get the vdom loaded
        let _ = dom.rebuild();

        if let Err(err) = websys_dom.rehydrate(&dom) {
            log::error!(
                "Rehydration failed {:?}. Rebuild DOM into element from scratch",
                &err
            );

            websys_dom.root.set_text_content(None);

            // errrrr we should split rebuild into two phases
            // one that initializes things and one that produces edits
            let edits = dom.rebuild();

            websys_dom.apply_edits(edits.edits);
        }
    } else {
        let edits = dom.rebuild();
        websys_dom.apply_edits(edits.edits);
    }

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

    loop {
        log::trace!("waiting for work");
        // 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;

        log::trace!("working..");

        // 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 edit in mutations {
            // actually apply our changes during the animation frame
            websys_dom.apply_edits(edit.edits);
        }
    }
}