Browse Source

Call handlers from context of a runtime and scope

Jonathan Kelley 1 năm trước cách đây
mục cha
commit
8323e45970

+ 19 - 14
examples/video_stream.rs

@@ -31,28 +31,33 @@ fn main() {
 }
 
 fn app(cx: Scope) -> Element {
-    use_asset_handler(cx, move |request: &AssetRequest| {
-        let request = request.clone();
-        async move {
+    use_asset_handler(cx, "videos", move |request, responder| {
+        // Using dioxus::spawn works, but is slower than a dedicated thread
+        tokio::task::spawn(async move {
             let video_file = PathBuf::from(VIDEO_PATH);
             let mut file = tokio::fs::File::open(&video_file).await.unwrap();
-            let response: Option<Response<Cow<'static, [u8]>>> =
-                match get_stream_response(&mut file, &request).await {
-                    Ok(response) => Some(response.map(Cow::Owned)),
-                    Err(err) => {
-                        eprintln!("Error: {}", err);
-                        None
-                    }
-                };
-            response
-        }
+
+            match get_stream_response(&mut file, &request).await {
+                Ok(response) => responder.respond(response),
+                Err(err) => eprintln!("Error: {}", err),
+            }
+        });
     });
 
     render! {
-        div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } }
+        div {
+            video {
+                src: "/videos/test_video.mp4",
+                autoplay: true,
+                controls: true,
+                width: 640,
+                height: 480
+            }
+        }
     }
 }
 
+/// This was taken from wry's example
 async fn get_stream_response(
     asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
     request: &AssetRequest,

+ 21 - 0
packages/core/src/runtime.rs

@@ -87,6 +87,16 @@ impl Runtime {
         self.scope_stack.borrow().last().copied()
     }
 
+    /// Call this function with the current scope set to the given scope
+    ///
+    /// Useful in a limited number of scenarios, not public.
+    pub(crate) fn with_scope<O>(&self, id: ScopeId, f: impl FnOnce() -> O) -> O {
+        self.scope_stack.borrow_mut().push(id);
+        let o = f();
+        self.scope_stack.borrow_mut().pop();
+        o
+    }
+
     /// Get the context for any scope given its ID
     ///
     /// This is useful for inserting or removing contexts from a scope, or rendering out its root node
@@ -137,6 +147,17 @@ impl RuntimeGuard {
         push_runtime(runtime.clone());
         Self(runtime)
     }
+
+    /// Run a function with a given runtime and scope in context
+    pub fn with<O>(runtime: Rc<Runtime>, scope: Option<ScopeId>, f: impl FnOnce() -> O) -> O {
+        let guard = Self::new(runtime.clone());
+        let o = match scope {
+            Some(scope) => Runtime::with_scope(&runtime, scope, f),
+            None => f(),
+        };
+        drop(guard);
+        o
+    }
 }
 
 impl Drop for RuntimeGuard {

+ 6 - 1
packages/core/src/scope_context.rs

@@ -318,11 +318,16 @@ pub fn spawn(fut: impl Future<Output = ()> + 'static) {
     with_current_scope(|cx| cx.spawn(fut));
 }
 
+/// Spawn a future on a component given its [`ScopeId`].
+pub fn spawn_at(fut: impl Future<Output = ()> + 'static, scope_id: ScopeId) -> Option<TaskId> {
+    with_runtime(|rt| rt.get_context(scope_id).unwrap().push_future(fut))
+}
+
 /// Spawn a future that Dioxus won't clean up when this component is unmounted
 ///
 /// This is good for tasks that need to be run after the component has been dropped.
 pub fn spawn_forever(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
-    with_current_scope(|cx| cx.spawn_forever(fut))
+    spawn_at(fut, ScopeId(0))
 }
 
 /// Informs the scheduler that this task is no longer needed and should be removed.

+ 1 - 0
packages/desktop/src/app.rs

@@ -320,6 +320,7 @@ impl<P: 'static> App<P> {
         let Some(view) = self.webviews.get_mut(&id) else {
             return;
         };
+        println!("poll_vdom");
 
         view.poll_vdom();
     }

+ 43 - 117
packages/desktop/src/assets.rs

@@ -1,135 +1,61 @@
-use crate::DesktopContext;
-use slab::Slab;
-use std::{
-    borrow::Cow,
-    future::Future,
-    ops::Deref,
-    path::{Path, PathBuf},
-    pin::Pin,
-    rc::Rc,
-    sync::Arc,
-};
-use tokio::{
-    runtime::Handle,
-    sync::{OnceCell, RwLock},
-};
-use wry::http::{Request, Response};
+use dioxus_core::prelude::{Runtime, RuntimeGuard, ScopeId};
+use rustc_hash::FxHashMap;
+use std::{cell::RefCell, rc::Rc};
+use wry::{http::Request, RequestAsyncResponder};
 
-/// An arbitrary asset is an HTTP response containing a binary body.
-pub type AssetResponse = Response<Cow<'static, [u8]>>;
+///
+pub type AssetRequest = Request<Vec<u8>>;
 
-/// A future that returns an [`AssetResponse`]. This future may be spawned in a new thread,
-/// so it must be [`Send`], [`Sync`], and `'static`.
-pub trait AssetFuture: Future<Output = Option<AssetResponse>> + Send + Sync + 'static {}
-impl<T: Future<Output = Option<AssetResponse>> + Send + Sync + 'static> AssetFuture for T {}
-
-#[derive(Debug, Clone)]
-/// A request for an asset. This is a wrapper around [`Request<Vec<u8>>`] that provides methods specific to asset requests.
-pub struct AssetRequest {
-    pub(crate) path: PathBuf,
-    pub(crate) request: Arc<Request<Vec<u8>>>,
-}
-
-impl AssetRequest {
-    /// Get the path the asset request is for
-    pub fn path(&self) -> &Path {
-        &self.path
-    }
-}
-
-impl From<Request<Vec<u8>>> for AssetRequest {
-    fn from(request: Request<Vec<u8>>) -> Self {
-        let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
-            .expect("expected URL to be UTF-8 encoded");
-        let path = PathBuf::from(&*decoded);
-        Self {
-            request: Arc::new(request),
-            path,
-        }
-    }
-}
-
-impl Deref for AssetRequest {
-    type Target = Request<Vec<u8>>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.request
-    }
-}
-
-/// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`.
-/// This handler is stashed indefinitely in a context object, so it must be `'static`.
-pub trait AssetHandler<F: AssetFuture>: Send + Sync + 'static {
-    /// Handle an asset request, returning a future that either loads the asset, or returns `None`
-    fn handle_request(&self, request: &AssetRequest) -> F;
+pub struct AssetHandler {
+    f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
+    scope: ScopeId,
 }
 
-impl<F: AssetFuture, T: Fn(&AssetRequest) -> F + Send + Sync + 'static> AssetHandler<F> for T {
-    fn handle_request(&self, request: &AssetRequest) -> F {
-        self(request)
-    }
-}
-
-type UserAssetHandler =
-    Box<dyn Fn(&AssetRequest) -> Pin<Box<dyn AssetFuture>> + Send + Sync + 'static>;
-
-type AssetHandlerRegistryInner = Slab<UserAssetHandler>;
-
 #[derive(Clone)]
-pub struct AssetHandlerRegistry(Arc<RwLock<AssetHandlerRegistryInner>>);
+pub struct AssetHandlerRegistry {
+    dom_rt: Rc<Runtime>,
+    handlers: Rc<RefCell<FxHashMap<String, AssetHandler>>>,
+}
 
 impl AssetHandlerRegistry {
-    pub fn new() -> Self {
-        AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new())))
-    }
-
-    pub async fn register_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
-        let mut registry = self.0.write().await;
-        registry.insert(Box::new(move |req| Box::pin(f.handle_request(req))))
+    pub fn new(dom_rt: Rc<Runtime>) -> Self {
+        AssetHandlerRegistry {
+            dom_rt,
+            handlers: Default::default(),
+        }
     }
 
-    pub async fn remove_handler(&self, id: usize) -> Option<()> {
-        let mut registry = self.0.write().await;
-        registry.try_remove(id).map(|_| ())
+    pub fn has_handler(&self, name: &str) -> bool {
+        self.handlers.borrow().contains_key(name)
     }
 
-    pub async fn try_handlers(&self, req: &AssetRequest) -> Option<AssetResponse> {
-        let registry = self.0.read().await;
-        for (_, handler) in registry.iter() {
-            if let Some(response) = handler(req).await {
-                return Some(response);
-            }
+    pub fn handle_request(
+        &self,
+        name: &str,
+        request: AssetRequest,
+        responder: RequestAsyncResponder,
+    ) {
+        if let Some(handler) = self.handlers.borrow().get(name) {
+            // make sure the runtime is alive for the duration of the handler
+            // We should do this for all the things - not just asset handlers
+            RuntimeGuard::with(self.dom_rt.clone(), Some(handler.scope), || {
+                (handler.f)(request, responder)
+            });
         }
-        None
     }
-}
 
-/// A handle to a registered asset handler.
-pub struct AssetHandlerHandle {
-    pub(crate) desktop: DesktopContext,
-    pub(crate) handler_id: Rc<OnceCell<usize>>,
-}
-
-impl AssetHandlerHandle {
-    /// Returns the ID for this handle.
-    ///
-    /// Because registering an ID is asynchronous, this may return `None` if the
-    /// registration has not completed yet.
-    pub fn handler_id(&self) -> Option<usize> {
-        self.handler_id.get().copied()
+    pub fn register_handler(
+        &self,
+        name: String,
+        f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
+        scope: ScopeId,
+    ) {
+        self.handlers
+            .borrow_mut()
+            .insert(name, AssetHandler { f, scope });
     }
-}
 
-impl Drop for AssetHandlerHandle {
-    fn drop(&mut self) {
-        let cell = Rc::clone(&self.handler_id);
-        let desktop = Rc::clone(&self.desktop);
-        tokio::task::block_in_place(move || {
-            Handle::current().block_on(async move {
-                if let Some(id) = cell.get() {
-                    desktop.asset_handlers.remove_handler(*id).await;
-                }
-            })
-        });
+    pub fn remove_handler(&self, name: &str) -> Option<AssetHandler> {
+        self.handlers.borrow_mut().remove(name)
     }
 }

+ 24 - 8
packages/desktop/src/desktop_context.rs

@@ -1,14 +1,17 @@
 use crate::{
     app::SharedContext,
-    assets::{AssetFuture, AssetHandlerRegistry},
+    assets::AssetHandlerRegistry,
     edits::EditQueue,
     ipc::{EventData, UserWindowEvent},
     query::QueryEngine,
     shortcut::{HotKey, ShortcutId, ShortcutRegistryError},
     webview::WebviewInstance,
-    AssetHandler, Config,
+    AssetRequest, Config,
+};
+use dioxus_core::{
+    prelude::{current_scope_id, ScopeId},
+    Mutations, VirtualDom,
 };
-use dioxus_core::{Mutations, VirtualDom};
 use dioxus_interpreter_js::binary_protocol::Channel;
 use rustc_hash::FxHashMap;
 use slab::Slab;
@@ -18,7 +21,7 @@ use tao::{
     event_loop::EventLoopWindowTarget,
     window::{Fullscreen as WryFullscreen, Window, WindowId},
 };
-use wry::WebView;
+use wry::{RequestAsyncResponder, WebView};
 
 #[cfg(target_os = "ios")]
 use tao::platform::ios::WindowExtIOS;
@@ -244,17 +247,30 @@ impl DesktopService {
     }
 
     /// Provide a callback to handle asset loading yourself.
+    /// If the ScopeId isn't provided, defaults to a global handler.
+    /// Note that the handler is namespaced by name, not ScopeId.
+    ///
+    /// When the component is dropped, the handler is removed.
     ///
     /// See [`use_asset_handle`](crate::use_asset_handle) for a convenient hook.
-    pub async fn register_asset_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
-        self.asset_handlers.register_handler(f).await
+    pub fn register_asset_handler(
+        &self,
+        name: String,
+        f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
+        scope: Option<ScopeId>,
+    ) {
+        self.asset_handlers.register_handler(
+            name,
+            f,
+            scope.unwrap_or(current_scope_id().unwrap_or(ScopeId(0))),
+        )
     }
 
     /// Removes an asset handler by its identifier.
     ///
     /// Returns `None` if the handler did not exist.
-    pub async fn remove_asset_handler(&self, id: usize) -> Option<()> {
-        self.asset_handlers.remove_handler(id).await
+    pub fn remove_asset_handler(&self, name: &str) -> Option<()> {
+        self.asset_handlers.remove_handler(name).map(|_| ())
     }
 
     /// Push an objc view to the window

+ 20 - 16
packages/desktop/src/hooks.rs

@@ -3,8 +3,8 @@ use crate::{
     ShortcutHandle, ShortcutRegistryError, WryEventHandler,
 };
 use dioxus_core::ScopeState;
-use std::rc::Rc;
 use tao::{event::Event, event_loop::EventLoopWindowTarget};
+use wry::RequestAsyncResponder;
 
 /// Get an imperative handle to the current window
 pub fn use_window(cx: &ScopeState) -> &DesktopContext {
@@ -34,24 +34,28 @@ pub fn use_wry_event_handler(
 ///
 /// The callback takes a path as requested by the web view, and it should return `Some(response)`
 /// if you want to load the asset, and `None` if you want to fallback on the default behavior.
-pub fn use_asset_handler<F: AssetFuture>(
+pub fn use_asset_handler(
     cx: &ScopeState,
-    handler: impl AssetHandler<F>,
-) -> &AssetHandlerHandle {
+    name: &str,
+    handler: impl Fn(AssetRequest, RequestAsyncResponder) + 'static,
+) {
     cx.use_hook(|| {
-        let desktop = crate::window();
-        let handler_id = Rc::new(tokio::sync::OnceCell::new());
-        let handler_id_ref = Rc::clone(&handler_id);
-        let desktop_ref = Rc::clone(&desktop);
-        cx.push_future(async move {
-            let id = desktop.asset_handlers.register_handler(handler).await;
-            handler_id.set(id).unwrap();
-        });
-        AssetHandlerHandle {
-            desktop: desktop_ref,
-            handler_id: handler_id_ref,
+        crate::window().asset_handlers.register_handler(
+            name.to_string(),
+            Box::new(handler),
+            cx.scope_id(),
+        );
+
+        Handler(name.to_string())
+    });
+
+    // todo: can we just put ondrop in core?
+    struct Handler(String);
+    impl Drop for Handler {
+        fn drop(&mut self) {
+            _ = crate::window().asset_handlers.remove_handler(&self.0);
         }
-    })
+    }
 }
 
 /// Get a closure that executes any JavaScript in the WebView context.

+ 2 - 1
packages/desktop/src/lib.rs

@@ -38,7 +38,7 @@ pub use tao::window::WindowBuilder;
 pub use wry;
 
 // Public exports
-pub use assets::{AssetFuture, AssetHandler, AssetRequest, AssetResponse};
+pub use assets::AssetRequest;
 pub use cfg::{Config, WindowCloseBehaviour};
 pub use desktop_context::{
     window, DesktopContext, DesktopService, WryEventHandler, WryEventHandlerId,
@@ -46,3 +46,4 @@ pub use desktop_context::{
 pub use hooks::{use_asset_handler, use_global_shortcut, use_window, use_wry_event_handler};
 pub use menubar::build_default_menu_bar;
 pub use shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError};
+pub use wry::RequestAsyncResponder;

+ 72 - 70
packages/desktop/src/protocol.rs

@@ -1,8 +1,5 @@
 use crate::{assets::*, edits::EditQueue};
-use std::{
-    borrow::Cow,
-    path::{Path, PathBuf},
-};
+use std::path::{Path, PathBuf};
 use wry::{
     http::{status::StatusCode, Request, Response},
     RequestAsyncResponder, Result,
@@ -11,69 +8,6 @@ use wry::{
 static MINIFIED: &str = include_str!("./minified.js");
 static DEFAULT_INDEX: &str = include_str!("./index.html");
 
-// todo: clean this up a bit
-#[allow(clippy::too_many_arguments)]
-pub(super) async fn desktop_handler(
-    request: Request<Vec<u8>>,
-    custom_head: Option<String>,
-    custom_index: Option<String>,
-    root_name: &str,
-    asset_handlers: &AssetHandlerRegistry,
-    edit_queue: &EditQueue,
-    headless: bool,
-    responder: RequestAsyncResponder,
-) {
-    let request = AssetRequest::from(request);
-
-    // If the request is for the root, we'll serve the index.html file.
-    if request.uri().path() == "/" {
-        match build_index_file(custom_index, custom_head, root_name, headless) {
-            Ok(response) => return responder.respond(response),
-            Err(err) => return tracing::error!("error building response: {}", err),
-        }
-    }
-
-    // If the request is asking for edits (ie binary protocol streaming, do that)
-    if request.uri().path().trim_matches('/') == "edits" {
-        return edit_queue.handle_request(responder);
-    }
-
-    // If the user provided a custom asset handler, then call it and return the response if the request was handled.
-    // todo(jon): I dont want this function to be async - we can probably just use a drop handler on the responder
-    if let Some(response) = asset_handlers.try_handlers(&request).await {
-        return responder.respond(response);
-    }
-
-    // Else, try to serve a file from the filesystem.
-    match serve_from_fs(request) {
-        Ok(res) => responder.respond(res),
-        Err(e) => tracing::error!("Error serving request from filesystem {}", e),
-    }
-}
-
-fn serve_from_fs(request: AssetRequest) -> Result<AssetResponse> {
-    // If the path is relative, we'll try to serve it from the assets directory.
-    let mut asset = get_asset_root_or_default().join(&request.path);
-
-    // If we can't find it, make it absolute and try again
-    if !asset.exists() {
-        asset = PathBuf::from("/").join(request.path);
-    }
-
-    if asset.exists() {
-        let content_type = get_mime_from_path(&asset)?;
-        let asset = std::fs::read(asset)?;
-
-        Ok(Response::builder()
-            .header("Content-Type", content_type)
-            .body(Cow::from(asset))?)
-    } else {
-        Ok(Response::builder()
-            .status(StatusCode::NOT_FOUND)
-            .body(Cow::from(String::from("Not Found").into_bytes()))?)
-    }
-}
-
 /// Build the index.html file we use for bootstrapping a new app
 ///
 /// We use wry/webview by building a special index.html that forms a bridge between the webview and your rust code
@@ -83,12 +17,18 @@ fn serve_from_fs(request: AssetRequest) -> Result<AssetResponse> {
 /// mess with UI elements. We make this decision since other renderers like LiveView are very separate and can
 /// never properly bridge the gap. Eventually of course, the idea is to build a custom CSS/HTML renderer where you
 /// *do* have native control over elements, but that still won't work with liveview.
-fn build_index_file(
-    custom_index: Option<String>,
+pub(super) fn index_request(
+    request: &Request<Vec<u8>>,
     custom_head: Option<String>,
+    custom_index: Option<String>,
     root_name: &str,
     headless: bool,
-) -> std::result::Result<Response<Vec<u8>>, wry::http::Error> {
+) -> Option<Response<Vec<u8>>> {
+    // If the request is for the root, we'll serve the index.html file.
+    if request.uri().path() != "/" {
+        return None;
+    }
+
     // Load a custom index file if provided
     let mut index = custom_index.unwrap_or_else(|| DEFAULT_INDEX.to_string());
 
@@ -110,6 +50,68 @@ fn build_index_file(
         .header("Content-Type", "text/html")
         .header("Access-Control-Allow-Origin", "*")
         .body(index.into())
+        .ok()
+}
+
+/// Handle a request from the webview
+///
+/// - Tries to stream edits if they're requested.
+/// - If that doesn't match, tries a user provided asset handler
+/// - If that doesn't match, tries to serve a file from the filesystem
+pub(super) fn desktop_handler(
+    request: Request<Vec<u8>>,
+    asset_handlers: AssetHandlerRegistry,
+    edit_queue: &EditQueue,
+    responder: RequestAsyncResponder,
+) {
+    // If the request is asking for edits (ie binary protocol streaming, do that)
+    if request.uri().path().trim_matches('/') == "edits" {
+        return edit_queue.handle_request(responder);
+    }
+
+    // If the user provided a custom asset handler, then call it and return the response if the request was handled.
+    // The path is the first part of the URI, so we need to trim the leading slash.
+    let path = PathBuf::from(
+        urlencoding::decode(request.uri().path().trim_start_matches('/'))
+            .expect("expected URL to be UTF-8 encoded")
+            .as_ref(),
+    );
+
+    let Some(name) = path.parent() else {
+        return tracing::error!("Asset request has no root {path:?}");
+    };
+
+    if let Some(name) = name.to_str() {
+        if asset_handlers.has_handler(name) {
+            return asset_handlers.handle_request(name, request, responder);
+        }
+    }
+
+    // Else, try to serve a file from the filesystem.
+    match serve_from_fs(path) {
+        Ok(res) => responder.respond(res),
+        Err(e) => tracing::error!("Error serving request from filesystem {}", e),
+    }
+}
+
+fn serve_from_fs(path: PathBuf) -> Result<Response<Vec<u8>>> {
+    // If the path is relative, we'll try to serve it from the assets directory.
+    let mut asset = get_asset_root_or_default().join(&path);
+
+    // If we can't find it, make it absolute and try again
+    if !asset.exists() {
+        asset = PathBuf::from("/").join(path);
+    }
+
+    if !asset.exists() {
+        return Ok(Response::builder()
+            .status(StatusCode::NOT_FOUND)
+            .body(String::from("Not Found").into_bytes())?);
+    }
+
+    Ok(Response::builder()
+        .header("Content-Type", get_mime_from_path(&asset)?)
+        .body(std::fs::read(asset)?)?)
 }
 
 /// Construct the inline script that boots up the page and bridges the webview with rust code.

+ 54 - 47
packages/desktop/src/webview.rs

@@ -11,7 +11,7 @@ use crate::{
 use dioxus_core::VirtualDom;
 use futures_util::{pin_mut, FutureExt};
 use std::{rc::Rc, task::Waker};
-use wry::{WebContext, WebViewBuilder};
+use wry::{RequestAsyncResponder, WebContext, WebViewBuilder};
 
 pub struct WebviewInstance {
     pub dom: VirtualDom,
@@ -33,12 +33,6 @@ impl WebviewInstance {
             build_menu_bar(build_default_menu_bar(), &window);
         }
 
-        let window_id = window.id();
-        let file_handler = cfg.file_drop_handler.take();
-        let custom_head = cfg.custom_head.clone();
-        let index_file = cfg.custom_index.clone();
-        let root_name = cfg.root_name.clone();
-
         // We assume that if the icon is None in cfg, then the user just didnt set it
         if cfg.window.window.window_icon.is_none() {
             window.set_window_icon(Some(
@@ -53,52 +47,65 @@ impl WebviewInstance {
 
         let mut web_context = WebContext::new(cfg.data_dir.clone());
         let edit_queue = EditQueue::default();
+        let asset_handlers = AssetHandlerRegistry::new(dom.runtime());
         let headless = !cfg.window.window.visible;
-        let asset_handlers = AssetHandlerRegistry::new();
-        let asset_handlers_ref = asset_handlers.clone();
+
+        // Rust :(
+        let window_id = window.id();
+        let file_handler = cfg.file_drop_handler.take();
+        let custom_head = cfg.custom_head.clone();
+        let index_file = cfg.custom_index.clone();
+        let root_name = cfg.root_name.clone();
+        let asset_handlers_ = asset_handlers.clone();
+        let edit_queue_ = edit_queue.clone();
+        let proxy_ = shared.proxy.clone();
+
+        let request_handler = move |request, responder: RequestAsyncResponder| {
+            // Try to serve the index file first
+            let index_bytes = protocol::index_request(
+                &request,
+                custom_head.clone(),
+                index_file.clone(),
+                &root_name,
+                headless,
+            );
+
+            // Otherwise, try to serve an asset, either from the user or the filesystem
+            match index_bytes {
+                Some(body) => return responder.respond(body),
+                None => {
+                    // we need to do this in the context of the dioxus runtime since the user gave us these closures
+                    protocol::desktop_handler(
+                        request,
+                        asset_handlers_.clone(),
+                        &edit_queue_,
+                        responder,
+                    );
+                }
+            }
+        };
+
+        let ipc_handler = move |payload: String| {
+            // defer the event to the main thread
+            if let Ok(message) = serde_json::from_str(&payload) {
+                _ = proxy_.send_event(UserWindowEvent(EventData::Ipc(message), window_id));
+            }
+        };
+
+        let file_drop_handler = move |event| {
+            file_handler
+                .as_ref()
+                .map(|handler| handler(window_id, event))
+                .unwrap_or_default()
+        };
 
         let mut webview = WebViewBuilder::new(&window)
             .with_transparent(cfg.window.window.transparent)
             .with_url("dioxus://index.html/")
             .unwrap()
-            .with_ipc_handler({
-                let proxy = shared.proxy.clone();
-                move |payload: String| {
-                    // defer the event to the main thread
-                    if let Ok(message) = serde_json::from_str(&payload) {
-                        _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window_id));
-                    }
-                }
-            })
-            .with_asynchronous_custom_protocol(String::from("dioxus"), {
-                let edit_queue = edit_queue.clone();
-                move |request, responder| {
-                    let custom_head = custom_head.clone();
-                    let index_file = index_file.clone();
-                    let root_name = root_name.clone();
-                    let asset_handlers_ref = asset_handlers_ref.clone();
-                    let edit_queue = edit_queue.clone();
-                    tokio::spawn(async move {
-                        protocol::desktop_handler(
-                            request,
-                            custom_head.clone(),
-                            index_file.clone(),
-                            &root_name,
-                            &asset_handlers_ref,
-                            &edit_queue,
-                            headless,
-                            responder,
-                        )
-                        .await;
-                    });
-                }
-            })
-            .with_file_drop_handler(move |event| {
-                file_handler
-                    .as_ref()
-                    .map(|handler| handler(window_id, event))
-                    .unwrap_or_default()
-            })
+            .with_ipc_handler(ipc_handler)
+            .with_asynchronous_custom_protocol(String::from("dioxus"), request_handler)
+            .with_file_drop_handler(file_drop_handler)
             .with_web_context(&mut web_context);
 
         #[cfg(windows)]