mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Call handlers from context of a runtime and scope
This commit is contained in:
parent
bc857bf339
commit
8323e45970
10 changed files with 270 additions and 282 deletions
|
@ -31,28 +31,33 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
use_asset_handler(cx, move |request: &AssetRequest| {
|
use_asset_handler(cx, "videos", move |request, responder| {
|
||||||
let request = request.clone();
|
// Using dioxus::spawn works, but is slower than a dedicated thread
|
||||||
async move {
|
tokio::task::spawn(async move {
|
||||||
let video_file = PathBuf::from(VIDEO_PATH);
|
let video_file = PathBuf::from(VIDEO_PATH);
|
||||||
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
|
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 {
|
match get_stream_response(&mut file, &request).await {
|
||||||
Ok(response) => Some(response.map(Cow::Owned)),
|
Ok(response) => responder.respond(response),
|
||||||
Err(err) => {
|
Err(err) => eprintln!("Error: {}", err),
|
||||||
eprintln!("Error: {}", err);
|
}
|
||||||
None
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
response
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
render! {
|
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(
|
async fn get_stream_response(
|
||||||
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
|
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
|
||||||
request: &AssetRequest,
|
request: &AssetRequest,
|
||||||
|
|
|
@ -87,6 +87,16 @@ impl Runtime {
|
||||||
self.scope_stack.borrow().last().copied()
|
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
|
/// 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
|
/// 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());
|
push_runtime(runtime.clone());
|
||||||
Self(runtime)
|
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 {
|
impl Drop for RuntimeGuard {
|
||||||
|
|
|
@ -318,11 +318,16 @@ pub fn spawn(fut: impl Future<Output = ()> + 'static) {
|
||||||
with_current_scope(|cx| cx.spawn(fut));
|
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
|
/// 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.
|
/// 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> {
|
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.
|
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||||
|
|
|
@ -320,6 +320,7 @@ impl<P: 'static> App<P> {
|
||||||
let Some(view) = self.webviews.get_mut(&id) else {
|
let Some(view) = self.webviews.get_mut(&id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
println!("poll_vdom");
|
||||||
|
|
||||||
view.poll_vdom();
|
view.poll_vdom();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,135 +1,61 @@
|
||||||
use crate::DesktopContext;
|
use dioxus_core::prelude::{Runtime, RuntimeGuard, ScopeId};
|
||||||
use slab::Slab;
|
use rustc_hash::FxHashMap;
|
||||||
use std::{
|
use std::{cell::RefCell, rc::Rc};
|
||||||
borrow::Cow,
|
use wry::{http::Request, RequestAsyncResponder};
|
||||||
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};
|
|
||||||
|
|
||||||
/// 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,
|
pub struct AssetHandler {
|
||||||
/// so it must be [`Send`], [`Sync`], and `'static`.
|
f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
|
||||||
pub trait AssetFuture: Future<Output = Option<AssetResponse>> + Send + Sync + 'static {}
|
scope: ScopeId,
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
#[derive(Clone)]
|
||||||
pub struct AssetHandlerRegistry(Arc<RwLock<AssetHandlerRegistryInner>>);
|
pub struct AssetHandlerRegistry {
|
||||||
|
dom_rt: Rc<Runtime>,
|
||||||
|
handlers: Rc<RefCell<FxHashMap<String, AssetHandler>>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl AssetHandlerRegistry {
|
impl AssetHandlerRegistry {
|
||||||
pub fn new() -> Self {
|
pub fn new(dom_rt: Rc<Runtime>) -> Self {
|
||||||
AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new())))
|
AssetHandlerRegistry {
|
||||||
}
|
dom_rt,
|
||||||
|
handlers: Default::default(),
|
||||||
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 async fn remove_handler(&self, id: usize) -> Option<()> {
|
|
||||||
let mut registry = self.0.write().await;
|
|
||||||
registry.try_remove(id).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
}
|
||||||
}
|
|
||||||
}
|
pub fn has_handler(&self, name: &str) -> bool {
|
||||||
|
self.handlers.borrow().contains_key(name)
|
||||||
/// A handle to a registered asset handler.
|
}
|
||||||
pub struct AssetHandlerHandle {
|
|
||||||
pub(crate) desktop: DesktopContext,
|
pub fn handle_request(
|
||||||
pub(crate) handler_id: Rc<OnceCell<usize>>,
|
&self,
|
||||||
}
|
name: &str,
|
||||||
|
request: AssetRequest,
|
||||||
impl AssetHandlerHandle {
|
responder: RequestAsyncResponder,
|
||||||
/// Returns the ID for this handle.
|
) {
|
||||||
///
|
if let Some(handler) = self.handlers.borrow().get(name) {
|
||||||
/// Because registering an ID is asynchronous, this may return `None` if the
|
// make sure the runtime is alive for the duration of the handler
|
||||||
/// registration has not completed yet.
|
// We should do this for all the things - not just asset handlers
|
||||||
pub fn handler_id(&self) -> Option<usize> {
|
RuntimeGuard::with(self.dom_rt.clone(), Some(handler.scope), || {
|
||||||
self.handler_id.get().copied()
|
(handler.f)(request, responder)
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
impl Drop for AssetHandlerHandle {
|
|
||||||
fn drop(&mut self) {
|
pub fn register_handler(
|
||||||
let cell = Rc::clone(&self.handler_id);
|
&self,
|
||||||
let desktop = Rc::clone(&self.desktop);
|
name: String,
|
||||||
tokio::task::block_in_place(move || {
|
f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
|
||||||
Handle::current().block_on(async move {
|
scope: ScopeId,
|
||||||
if let Some(id) = cell.get() {
|
) {
|
||||||
desktop.asset_handlers.remove_handler(*id).await;
|
self.handlers
|
||||||
}
|
.borrow_mut()
|
||||||
})
|
.insert(name, AssetHandler { f, scope });
|
||||||
});
|
}
|
||||||
|
|
||||||
|
pub fn remove_handler(&self, name: &str) -> Option<AssetHandler> {
|
||||||
|
self.handlers.borrow_mut().remove(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
app::SharedContext,
|
app::SharedContext,
|
||||||
assets::{AssetFuture, AssetHandlerRegistry},
|
assets::AssetHandlerRegistry,
|
||||||
edits::EditQueue,
|
edits::EditQueue,
|
||||||
ipc::{EventData, UserWindowEvent},
|
ipc::{EventData, UserWindowEvent},
|
||||||
query::QueryEngine,
|
query::QueryEngine,
|
||||||
shortcut::{HotKey, ShortcutId, ShortcutRegistryError},
|
shortcut::{HotKey, ShortcutId, ShortcutRegistryError},
|
||||||
webview::WebviewInstance,
|
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 dioxus_interpreter_js::binary_protocol::Channel;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
|
@ -18,7 +21,7 @@ use tao::{
|
||||||
event_loop::EventLoopWindowTarget,
|
event_loop::EventLoopWindowTarget,
|
||||||
window::{Fullscreen as WryFullscreen, Window, WindowId},
|
window::{Fullscreen as WryFullscreen, Window, WindowId},
|
||||||
};
|
};
|
||||||
use wry::WebView;
|
use wry::{RequestAsyncResponder, WebView};
|
||||||
|
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
use tao::platform::ios::WindowExtIOS;
|
use tao::platform::ios::WindowExtIOS;
|
||||||
|
@ -244,17 +247,30 @@ impl DesktopService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provide a callback to handle asset loading yourself.
|
/// 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.
|
/// 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 {
|
pub fn register_asset_handler(
|
||||||
self.asset_handlers.register_handler(f).await
|
&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.
|
/// Removes an asset handler by its identifier.
|
||||||
///
|
///
|
||||||
/// Returns `None` if the handler did not exist.
|
/// Returns `None` if the handler did not exist.
|
||||||
pub async fn remove_asset_handler(&self, id: usize) -> Option<()> {
|
pub fn remove_asset_handler(&self, name: &str) -> Option<()> {
|
||||||
self.asset_handlers.remove_handler(id).await
|
self.asset_handlers.remove_handler(name).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push an objc view to the window
|
/// Push an objc view to the window
|
||||||
|
|
|
@ -3,8 +3,8 @@ use crate::{
|
||||||
ShortcutHandle, ShortcutRegistryError, WryEventHandler,
|
ShortcutHandle, ShortcutRegistryError, WryEventHandler,
|
||||||
};
|
};
|
||||||
use dioxus_core::ScopeState;
|
use dioxus_core::ScopeState;
|
||||||
use std::rc::Rc;
|
|
||||||
use tao::{event::Event, event_loop::EventLoopWindowTarget};
|
use tao::{event::Event, event_loop::EventLoopWindowTarget};
|
||||||
|
use wry::RequestAsyncResponder;
|
||||||
|
|
||||||
/// Get an imperative handle to the current window
|
/// Get an imperative handle to the current window
|
||||||
pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
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)`
|
/// 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.
|
/// 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,
|
cx: &ScopeState,
|
||||||
handler: impl AssetHandler<F>,
|
name: &str,
|
||||||
) -> &AssetHandlerHandle {
|
handler: impl Fn(AssetRequest, RequestAsyncResponder) + 'static,
|
||||||
|
) {
|
||||||
cx.use_hook(|| {
|
cx.use_hook(|| {
|
||||||
let desktop = crate::window();
|
crate::window().asset_handlers.register_handler(
|
||||||
let handler_id = Rc::new(tokio::sync::OnceCell::new());
|
name.to_string(),
|
||||||
let handler_id_ref = Rc::clone(&handler_id);
|
Box::new(handler),
|
||||||
let desktop_ref = Rc::clone(&desktop);
|
cx.scope_id(),
|
||||||
cx.push_future(async move {
|
);
|
||||||
let id = desktop.asset_handlers.register_handler(handler).await;
|
|
||||||
handler_id.set(id).unwrap();
|
Handler(name.to_string())
|
||||||
});
|
});
|
||||||
AssetHandlerHandle {
|
|
||||||
desktop: desktop_ref,
|
// todo: can we just put ondrop in core?
|
||||||
handler_id: handler_id_ref,
|
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.
|
/// Get a closure that executes any JavaScript in the WebView context.
|
||||||
|
|
|
@ -38,7 +38,7 @@ pub use tao::window::WindowBuilder;
|
||||||
pub use wry;
|
pub use wry;
|
||||||
|
|
||||||
// Public exports
|
// Public exports
|
||||||
pub use assets::{AssetFuture, AssetHandler, AssetRequest, AssetResponse};
|
pub use assets::AssetRequest;
|
||||||
pub use cfg::{Config, WindowCloseBehaviour};
|
pub use cfg::{Config, WindowCloseBehaviour};
|
||||||
pub use desktop_context::{
|
pub use desktop_context::{
|
||||||
window, DesktopContext, DesktopService, WryEventHandler, WryEventHandlerId,
|
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 hooks::{use_asset_handler, use_global_shortcut, use_window, use_wry_event_handler};
|
||||||
pub use menubar::build_default_menu_bar;
|
pub use menubar::build_default_menu_bar;
|
||||||
pub use shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
pub use shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||||
|
pub use wry::RequestAsyncResponder;
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
use crate::{assets::*, edits::EditQueue};
|
use crate::{assets::*, edits::EditQueue};
|
||||||
use std::{
|
use std::path::{Path, PathBuf};
|
||||||
borrow::Cow,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
use wry::{
|
use wry::{
|
||||||
http::{status::StatusCode, Request, Response},
|
http::{status::StatusCode, Request, Response},
|
||||||
RequestAsyncResponder, Result,
|
RequestAsyncResponder, Result,
|
||||||
|
@ -11,69 +8,6 @@ use wry::{
|
||||||
static MINIFIED: &str = include_str!("./minified.js");
|
static MINIFIED: &str = include_str!("./minified.js");
|
||||||
static DEFAULT_INDEX: &str = include_str!("./index.html");
|
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
|
/// 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
|
/// 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
|
/// 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
|
/// 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.
|
/// *do* have native control over elements, but that still won't work with liveview.
|
||||||
fn build_index_file(
|
pub(super) fn index_request(
|
||||||
custom_index: Option<String>,
|
request: &Request<Vec<u8>>,
|
||||||
custom_head: Option<String>,
|
custom_head: Option<String>,
|
||||||
|
custom_index: Option<String>,
|
||||||
root_name: &str,
|
root_name: &str,
|
||||||
headless: bool,
|
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
|
// Load a custom index file if provided
|
||||||
let mut index = custom_index.unwrap_or_else(|| DEFAULT_INDEX.to_string());
|
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("Content-Type", "text/html")
|
||||||
.header("Access-Control-Allow-Origin", "*")
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
.body(index.into())
|
.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.
|
/// Construct the inline script that boots up the page and bridges the webview with rust code.
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
||||||
use dioxus_core::VirtualDom;
|
use dioxus_core::VirtualDom;
|
||||||
use futures_util::{pin_mut, FutureExt};
|
use futures_util::{pin_mut, FutureExt};
|
||||||
use std::{rc::Rc, task::Waker};
|
use std::{rc::Rc, task::Waker};
|
||||||
use wry::{WebContext, WebViewBuilder};
|
use wry::{RequestAsyncResponder, WebContext, WebViewBuilder};
|
||||||
|
|
||||||
pub struct WebviewInstance {
|
pub struct WebviewInstance {
|
||||||
pub dom: VirtualDom,
|
pub dom: VirtualDom,
|
||||||
|
@ -33,12 +33,6 @@ impl WebviewInstance {
|
||||||
build_menu_bar(build_default_menu_bar(), &window);
|
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
|
// 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() {
|
if cfg.window.window.window_icon.is_none() {
|
||||||
window.set_window_icon(Some(
|
window.set_window_icon(Some(
|
||||||
|
@ -53,52 +47,65 @@ impl WebviewInstance {
|
||||||
|
|
||||||
let mut web_context = WebContext::new(cfg.data_dir.clone());
|
let mut web_context = WebContext::new(cfg.data_dir.clone());
|
||||||
let edit_queue = EditQueue::default();
|
let edit_queue = EditQueue::default();
|
||||||
|
let asset_handlers = AssetHandlerRegistry::new(dom.runtime());
|
||||||
let headless = !cfg.window.window.visible;
|
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)
|
let mut webview = WebViewBuilder::new(&window)
|
||||||
.with_transparent(cfg.window.window.transparent)
|
.with_transparent(cfg.window.window.transparent)
|
||||||
.with_url("dioxus://index.html/")
|
.with_url("dioxus://index.html/")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_ipc_handler({
|
.with_ipc_handler(ipc_handler)
|
||||||
let proxy = shared.proxy.clone();
|
.with_asynchronous_custom_protocol(String::from("dioxus"), request_handler)
|
||||||
move |payload: String| {
|
.with_file_drop_handler(file_drop_handler)
|
||||||
// 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_web_context(&mut web_context);
|
.with_web_context(&mut web_context);
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|
Loading…
Reference in a new issue