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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#![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
//! - controlled components
//! - 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 "work_with_deadline" 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

pub use crate::cfg::Config;
pub use crate::util::{use_eval, EvalResult};
use dioxus_core::{Element, Scope, VirtualDom};
use futures_util::{pin_mut, FutureExt, StreamExt};

mod cache;
mod cfg;
mod dom;
mod hot_reload;
mod util;

// Currently disabled since it actually slows down immediate rendering
// todo: only schedule non-immediate renders through ric/raf
// mod ric_raf;
// mod rehydrate;

/// 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| {
///     render!(div {"hello world"})
/// }
/// ```
pub fn launch(root_component: fn(Scope) -> Element) {
    launch_with_props(root_component, (), Config::default());
}

/// 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, ignore
/// use dioxus::prelude::*;
///
/// fn main() {
///     dioxus_web::launch_with_props(App, Config::new().pre_render(true));
/// }
///
/// fn app(cx: Scope) -> Element {
///     cx.render(rsx!{
///         h1 {"hello world!"}
///     })
/// }
/// ```
pub fn launch_cfg(root: fn(Scope) -> Element, config: Config) {
    launch_with_props(root, (), config)
}

/// 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::new()
///     );
/// }
///
/// #[derive(ParitalEq, Props)]
/// struct RootProps {
///     name: String
/// }
///
/// static App: Component<RootProps> = |cx| {
///     render!(div {"hello {cx.props.name}"})
/// }
/// ```
pub fn launch_with_props<T: 'static>(
    root_component: fn(Scope<T>) -> Element,
    root_properties: T,
    config: 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>(root: fn(Scope<T>) -> Element, root_props: T, cfg: Config) {
    log::info!("Starting up");

    let mut dom = VirtualDom::new_with_props(root, root_props);

    #[cfg(feature = "panic_hook")]
    if cfg.default_panic_hook {
        console_error_panic_hook::set_once();
    }

    let mut hotreload_rx = hot_reload::init();

    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 (tx, mut rx) = futures_channel::mpsc::unbounded();

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

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

    // if should_hydrate {
    // } else {
    {
        let edits = dom.rebuild();

        websys_dom.load_templates(&edits.templates);
        websys_dom.apply_edits(edits.edits);
    }

    // the mutations come back with nothing - we need to actually mount them
    websys_dom.mount();

    loop {
        log::debug!("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.
        let (mut res, template) = {
            let work = dom.wait_for_work().fuse();
            pin_mut!(work);

            futures_util::select! {
                _ = work => (None, None),
                new_template = hotreload_rx.next() => {
                    (None, new_template)
                }
                evt = rx.next() => (evt, None)
            }
        };

        if let Some(template) = template {
            dom.replace_template(template);
        }

        // Dequeue all of the events from the channel in send order
        // todo: we should re-order these if possible
        while let Some(evt) = res {
            dom.handle_event(evt.name.as_str(), evt.data, evt.element, evt.bubbles);
            res = rx.try_next().transpose().unwrap().ok();
        }

        // Todo: This is currently disabled because it has a negative impact on responce times for events but it could be re-enabled for tasks
        // Jank free rendering
        //
        // 1. wait for the browser to give us "idle" time
        // 2. During idle time, diff the dom
        // 3. Stop diffing if the deadline is exceded
        // 4. Wait for the animation frame to patch the dom

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

        // run the virtualdom work phase until the frame deadline is reached
        let edits = dom.render_immediate();

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

        websys_dom.load_templates(&edits.templates);
        websys_dom.apply_edits(edits.edits);
    }
}

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

//     #[cfg(feature = "hydrate")]
//     #[allow(unused_variables)]
//     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.template_mutations);
//     websys_dom.apply_edits(edits.edits);
// }