More cleanup work

This commit is contained in:
Jonathan Kelley 2024-01-04 17:21:33 -08:00
parent ec3eaa6b26
commit 2171263eda
No known key found for this signature in database
GPG key ID: 1FBB50F7EB0A08BE
11 changed files with 604 additions and 555 deletions

View file

@ -1,20 +1,21 @@
pub use crate::assets::{AssetFuture, AssetHandler, AssetRequest, AssetResponse};
pub use crate::cfg::{Config, WindowCloseBehaviour};
pub use crate::desktop_context::DesktopContext;
pub use crate::desktop_context::{
use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId,
};
use crate::desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
pub use crate::desktop_context::{window, DesktopService, WryEventHandler, WryEventHandlerId};
use crate::edits::{send_edits, EditQueue, WebviewQueue};
use crate::element::DesktopElement;
use crate::eval::init_eval;
use crate::events::{IpcMessage, IpcMethod};
use crate::file_upload;
pub use crate::protocol::{
use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse,
};
use crate::hooks::*;
use crate::query::QueryResult;
use crate::shortcut::GlobalHotKeyEvent;
use crate::shortcut::ShortcutRegistry;
pub use crate::shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
use crate::{
desktop_context::{EventData, UserWindowEvent, WindowEventHandlers},
webview::WebviewHandler,
};
use dioxus_core::*;
use dioxus_html::{event_bubbles, MountedData};
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
@ -168,7 +169,7 @@ impl<P: 'static> App<P> {
self.is_visible_before_start = cfg.window.window.visible;
let handler = create_new_window(
let handler = crate::webview::create_new_window(
cfg,
target,
&self.proxy,
@ -333,14 +334,12 @@ impl<P: 'static> App<P> {
let mut cx = std::task::Context::from_waker(&view.waker);
loop {
{
let fut = view.dom.wait_for_work();
pin_mut!(fut);
let fut = view.dom.wait_for_work();
pin_mut!(fut);
match fut.poll_unpin(&mut cx) {
std::task::Poll::Ready(_) => {}
std::task::Poll::Pending => break,
}
match fut.poll_unpin(&mut cx) {
std::task::Poll::Ready(_) => {}
std::task::Poll::Pending => break,
}
send_edits(view.dom.render_immediate(), &view.desktop_context);
@ -348,196 +347,6 @@ impl<P: 'static> App<P> {
}
}
pub fn create_new_window(
mut cfg: Config,
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
proxy: &EventLoopProxy<UserWindowEvent>,
dom: VirtualDom,
queue: &WebviewQueue,
event_handlers: &WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
) -> WebviewHandler {
let (webview, web_context, asset_handlers, edit_queue, window) =
crate::webview::build(&mut cfg, event_loop, proxy.clone());
let desktop_context = Rc::from(DesktopService::new(
window,
webview,
proxy.clone(),
event_loop.clone(),
queue.clone(),
event_handlers.clone(),
shortcut_manager,
edit_queue,
asset_handlers,
));
let cx = dom.base_scope();
cx.provide_context(desktop_context.clone());
// Init eval
init_eval(cx);
WebviewHandler {
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
waker: crate::waker::tao_waker(proxy, desktop_context.window.id()),
desktop_context,
dom,
_web_context: web_context,
}
}
pub struct WebviewHandler {
pub dom: VirtualDom,
pub desktop_context: DesktopContext,
pub waker: Waker,
// Wry assumes the webcontext is alive for the lifetime of the webview.
// We need to keep the webcontext alive, otherwise the webview will crash
_web_context: WebContext,
}
/// Send a list of mutations to the webview
pub fn send_edits(edits: Mutations, desktop_context: &DesktopContext) {
let mut channel = desktop_context.channel.borrow_mut();
let mut templates = desktop_context.templates.borrow_mut();
if let Some(bytes) = apply_edits(
edits,
&mut channel,
&mut templates,
&desktop_context.max_template_count,
) {
desktop_context.edit_queue.add_edits(bytes)
}
}
pub fn apply_edits(
mutations: Mutations,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) -> Option<Vec<u8>> {
use dioxus_core::Mutation::*;
if mutations.templates.is_empty() && mutations.edits.is_empty() {
return None;
}
for template in mutations.templates {
add_template(&template, channel, templates, max_template_count);
}
for edit in mutations.edits {
match edit {
AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
AssignId { path, id } => channel.assign_id(path, id.0 as u32),
CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
LoadTemplate { name, index, id } => {
if let Some(tmpl_id) = templates.get(name) {
channel.load_template(*tmpl_id, index as u16, id.0 as u32)
}
}
ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
SetAttribute {
name,
value,
id,
ns,
} => match value {
BorrowedAttributeValue::Text(txt) => {
channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
}
BorrowedAttributeValue::Float(f) => {
channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
}
BorrowedAttributeValue::Int(n) => {
channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
}
BorrowedAttributeValue::Bool(b) => channel.set_attribute(
id.0 as u32,
name,
if b { "true" } else { "false" },
ns.unwrap_or_default(),
),
BorrowedAttributeValue::None => {
channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
}
_ => unreachable!(),
},
SetText { value, id } => channel.set_text(id.0 as u32, value),
NewEventListener { name, id, .. } => {
channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
}
RemoveEventListener { name, id } => {
channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
}
Remove { id } => channel.remove(id.0 as u32),
PushRoot { id } => channel.push_root(id.0 as u32),
}
}
let bytes: Vec<_> = channel.export_memory().collect();
channel.reset();
Some(bytes)
}
pub fn add_template(
template: &Template<'static>,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) {
let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed);
for root in template.roots.iter() {
create_template_node(channel, root);
templates.insert(template.name.to_owned(), current_max_template_count);
}
channel.add_templates(current_max_template_count, template.roots.len() as u16);
max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) {
use TemplateNode::*;
match v {
Element {
tag,
namespace,
attrs,
children,
..
} => {
// Push the current node onto the stack
match namespace {
Some(ns) => channel.create_element_ns(tag, ns),
None => channel.create_element(tag),
}
// Set attributes on the current node
for attr in *attrs {
if let TemplateAttribute::Static {
name,
value,
namespace,
} = attr
{
channel.set_top_attribute(name, value, namespace.unwrap_or_default())
}
}
// Add each child to the stack
for child in *children {
create_template_node(channel, child);
}
// Add all children to the parent
channel.append_children_to_top(children.len() as u16);
}
Text { text } => channel.create_raw_text(text),
DynamicText { .. } => channel.create_raw_text("p"),
Dynamic { .. } => channel.add_placeholder(),
}
}
/// Different hide implementations per platform
#[allow(unused)]
pub fn hide_app_window(webview: &WebView) {

View file

@ -0,0 +1,138 @@
use crate::edits::EditQueue;
use crate::DesktopContext;
use dioxus_core::ScopeState;
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::{status::StatusCode, Request, Response},
Result,
};
/// An arbitrary asset is an HTTP response containing a binary body.
pub type AssetResponse = Response<Cow<'static, [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;
}
impl<F: AssetFuture, T: Fn(&AssetRequest) -> F + Send + Sync + 'static> AssetHandler<F> for T {
fn handle_request(&self, request: &AssetRequest) -> F {
self(request)
}
}
type AssetHandlerRegistryInner =
Slab<Box<dyn Fn(&AssetRequest) -> Pin<Box<dyn AssetFuture>> + Send + Sync + 'static>>;
#[derive(Clone)]
pub struct AssetHandlerRegistry(Arc<RwLock<AssetHandlerRegistryInner>>);
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 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
}
}
/// 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()
}
}
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;
}
})
});
}
}

View file

@ -1,10 +1,10 @@
use crate::protocol::AssetFuture;
use crate::protocol::AssetHandlerRegistry;
use crate::query::QueryEngine;
use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
use crate::AssetHandler;
use crate::Config;
use crate::{app::WebviewHandler, events::IpcMessage};
use crate::{assets::AssetFuture, edits::WebviewQueue};
use crate::{assets::AssetHandlerRegistry, edits::EditQueue};
use crate::{events::IpcMessage, webview::WebviewHandler};
use dioxus_core::ScopeState;
use dioxus_core::VirtualDom;
use dioxus_interpreter_js::binary_protocol::Channel;
@ -34,54 +34,6 @@ pub fn window() -> DesktopContext {
dioxus_core::prelude::consume_context().unwrap()
}
/// Get an imperative handle to the current window
pub fn use_window(cx: &ScopeState) -> &DesktopContext {
cx.use_hook(|| cx.consume_context::<DesktopContext>())
.as_ref()
.unwrap()
}
/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events.
/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made.
#[derive(Default, Clone)]
pub(crate) struct EditQueue {
queue: Arc<Mutex<Vec<Vec<u8>>>>,
responder: Arc<Mutex<Option<RequestAsyncResponder>>>,
}
impl Debug for EditQueue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EditQueue")
.field("queue", &self.queue)
.field("responder", {
&self.responder.lock().unwrap().as_ref().map(|_| ())
})
.finish()
}
}
impl EditQueue {
pub fn handle_request(&self, responder: RequestAsyncResponder) {
let mut queue = self.queue.lock().unwrap();
if let Some(bytes) = queue.pop() {
responder.respond(wry::http::Response::new(bytes));
} else {
*self.responder.lock().unwrap() = Some(responder);
}
}
pub fn add_edits(&self, edits: Vec<u8>) {
let mut responder = self.responder.lock().unwrap();
if let Some(responder) = responder.take() {
responder.respond(wry::http::Response::new(edits));
} else {
self.queue.lock().unwrap().push(edits);
}
}
}
pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
/// An imperative interface to the current window.
///
/// To get a handle to the current window, use the [`use_window`] hook.
@ -177,7 +129,7 @@ impl DesktopService {
///
/// Be careful to not create a cycle of windows, or you might leak memory.
pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak<DesktopService> {
let window = crate::app::create_new_window(
let window = crate::webview::create_new_window(
cfg,
&self.event_loop,
&self.proxy,
@ -456,28 +408,11 @@ impl WryWindowEventHandlerInner {
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_wry_event_handler(
cx: &ScopeState,
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
) -> &WryEventHandler {
cx.use_hook(move || {
let desktop = window();
let id = desktop.create_wry_event_handler(handler);
WryEventHandler {
handlers: desktop.event_handlers.clone(),
id,
}
})
}
/// A wry event handler that is scoped to the current component and window. The event handler will only receive events for the window it was created for and global events.
///
/// This will automatically be removed when the component is unmounted.
pub struct WryEventHandler {
handlers: WindowEventHandlers,
pub(crate) handlers: WindowEventHandlers,
/// The unique identifier of the event handler.
pub id: WryEventHandlerId,
}

View file

@ -0,0 +1,206 @@
use crate::assets::AssetHandlerRegistry;
use crate::query::QueryEngine;
use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
use crate::AssetHandler;
use crate::Config;
use crate::{assets::AssetFuture, DesktopContext};
use crate::{events::IpcMessage, webview::WebviewHandler};
use dioxus_core::{BorrowedAttributeValue, Template, TemplateAttribute, TemplateNode, VirtualDom};
use dioxus_core::{Mutations, ScopeState};
use dioxus_html::event_bubbles;
use dioxus_interpreter_js::binary_protocol::Channel;
use rustc_hash::FxHashMap;
use slab::Slab;
use std::{
cell::RefCell, fmt::Debug, fmt::Formatter, rc::Rc, rc::Weak, sync::atomic::AtomicU16,
sync::Arc, sync::Mutex,
};
use tao::{
event::Event,
event_loop::{EventLoopProxy, EventLoopWindowTarget},
window::{Fullscreen as WryFullscreen, Window, WindowId},
};
use wry::{RequestAsyncResponder, WebView};
pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events.
/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made.
#[derive(Default, Clone)]
pub(crate) struct EditQueue {
queue: Arc<Mutex<Vec<Vec<u8>>>>,
responder: Arc<Mutex<Option<RequestAsyncResponder>>>,
}
impl Debug for EditQueue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EditQueue")
.field("queue", &self.queue)
.field("responder", {
&self.responder.lock().unwrap().as_ref().map(|_| ())
})
.finish()
}
}
impl EditQueue {
pub fn handle_request(&self, responder: RequestAsyncResponder) {
let mut queue = self.queue.lock().unwrap();
if let Some(bytes) = queue.pop() {
responder.respond(wry::http::Response::new(bytes));
} else {
*self.responder.lock().unwrap() = Some(responder);
}
}
pub fn add_edits(&self, edits: Vec<u8>) {
let mut responder = self.responder.lock().unwrap();
if let Some(responder) = responder.take() {
responder.respond(wry::http::Response::new(edits));
} else {
self.queue.lock().unwrap().push(edits);
}
}
}
/// Send a list of mutations to the webview
pub fn send_edits(edits: Mutations, desktop_context: &DesktopContext) {
let mut channel = desktop_context.channel.borrow_mut();
let mut templates = desktop_context.templates.borrow_mut();
if let Some(bytes) = apply_edits(
edits,
&mut channel,
&mut templates,
&desktop_context.max_template_count,
) {
desktop_context.edit_queue.add_edits(bytes)
}
}
pub fn apply_edits(
mutations: Mutations,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) -> Option<Vec<u8>> {
use dioxus_core::Mutation::*;
if mutations.templates.is_empty() && mutations.edits.is_empty() {
return None;
}
for template in mutations.templates {
add_template(&template, channel, templates, max_template_count);
}
for edit in mutations.edits {
match edit {
AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
AssignId { path, id } => channel.assign_id(path, id.0 as u32),
CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
LoadTemplate { name, index, id } => {
if let Some(tmpl_id) = templates.get(name) {
channel.load_template(*tmpl_id, index as u16, id.0 as u32)
}
}
ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
SetAttribute {
name,
value,
id,
ns,
} => match value {
BorrowedAttributeValue::Text(txt) => {
channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
}
BorrowedAttributeValue::Float(f) => {
channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
}
BorrowedAttributeValue::Int(n) => {
channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
}
BorrowedAttributeValue::Bool(b) => channel.set_attribute(
id.0 as u32,
name,
if b { "true" } else { "false" },
ns.unwrap_or_default(),
),
BorrowedAttributeValue::None => {
channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
}
_ => unreachable!(),
},
SetText { value, id } => channel.set_text(id.0 as u32, value),
NewEventListener { name, id, .. } => {
channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
}
RemoveEventListener { name, id } => {
channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
}
Remove { id } => channel.remove(id.0 as u32),
PushRoot { id } => channel.push_root(id.0 as u32),
}
}
let bytes: Vec<_> = channel.export_memory().collect();
channel.reset();
Some(bytes)
}
pub fn add_template(
template: &Template<'static>,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) {
let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed);
for root in template.roots.iter() {
create_template_node(channel, root);
templates.insert(template.name.to_owned(), current_max_template_count);
}
channel.add_templates(current_max_template_count, template.roots.len() as u16);
max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) {
use TemplateNode::*;
match v {
Element {
tag,
namespace,
attrs,
children,
..
} => {
// Push the current node onto the stack
match namespace {
Some(ns) => channel.create_element_ns(tag, ns),
None => channel.create_element(tag),
}
// Set attributes on the current node
for attr in *attrs {
if let TemplateAttribute::Static {
name,
value,
namespace,
} = attr
{
channel.set_top_attribute(name, value, namespace.unwrap_or_default())
}
}
// Add each child to the stack
for child in *children {
create_template_node(channel, child);
}
// Add all children to the parent
channel.append_children_to_top(children.len() as u16);
}
Text { text } => channel.create_raw_text(text),
DynamicText { .. } => channel.create_raw_text("p"),
Dynamic { .. } => channel.add_placeholder(),
}
}

View file

@ -1 +1,54 @@
use std::rc::Rc;
use crate::assets::*;
use crate::{desktop_context::UserWindowEvent, window, DesktopContext, WryEventHandler};
use dioxus_core::ScopeState;
use tao::{event::Event, event_loop::EventLoopWindowTarget};
/// Get an imperative handle to the current window
pub fn use_window(cx: &ScopeState) -> &DesktopContext {
cx.use_hook(|| cx.consume_context::<DesktopContext>())
.as_ref()
.unwrap()
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_wry_event_handler(
cx: &ScopeState,
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
) -> &WryEventHandler {
cx.use_hook(move || {
let desktop = window();
let id = desktop.create_wry_event_handler(handler);
WryEventHandler {
handlers: desktop.event_handlers.clone(),
id,
}
})
}
/// Provide a callback to handle asset loading yourself.
///
/// 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>(
cx: &ScopeState,
handler: impl AssetHandler<F>,
) -> &AssetHandlerHandle {
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,
}
})
}

View file

@ -4,15 +4,17 @@
#![deny(missing_docs)]
mod app;
mod asset_handler;
mod assets;
mod cfg;
mod desktop_context;
mod edits;
mod element;
mod escape;
mod eval;
mod events;
mod file_upload;
mod hooks;
mod menubar;
#[cfg(any(target_os = "ios", target_os = "android"))]
mod mobile_shortcut;
mod protocol;
@ -21,25 +23,30 @@ mod shortcut;
mod waker;
mod webview;
pub use cfg::{Config, WindowCloseBehaviour};
pub use desktop_context::use_window;
pub use desktop_context::DesktopContext;
#[allow(deprecated)]
pub use desktop_context::{
use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId,
// Re-exports
pub use tao::{
self,
dpi::{LogicalSize, PhysicalSize},
window::WindowBuilder,
};
pub use wry;
// Public exports
pub use assets::{AssetFuture, AssetHandler, AssetRequest, AssetResponse};
pub use cfg::{Config, WindowCloseBehaviour};
pub use desktop_context::{
window, DesktopContext, DesktopService, WryEventHandler, WryEventHandlerId,
};
pub use hooks::{use_asset_handler, use_window, use_wry_event_handler};
pub use menubar::build_default_menu_bar;
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
#[allow(deprecated)]
use desktop_context::{EventData, UserWindowEvent};
use dioxus_core::*;
use events::IpcMethod;
pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse};
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
pub use tao;
pub use tao::dpi::{LogicalSize, PhysicalSize};
use tao::event::{Event, StartCause, WindowEvent};
pub use tao::window::WindowBuilder;
use tokio::runtime::Builder;
pub use webview::build_default_menu_bar;
pub use wry;
/// Launch the WebView and run the event loop.
///

View file

@ -0,0 +1,58 @@
use muda::{Menu, MenuItem, PredefinedMenuItem, Submenu};
use tao::window::Window;
pub fn build_menu_bar(menu: Menu, window: &Window) {
#[cfg(target_os = "windows")]
menu.init_for_hwnd(window_hwnd);
#[cfg(target_os = "linux")]
menu.init_for_gtk_window(&gtk_window, Some(&vertical_gtk_box));
#[cfg(target_os = "macos")]
menu.init_for_nsapp();
}
/// Builds a standard menu bar depending on the users platform. It may be used as a starting point
/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder).
/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste.
/// > The menu bar differs per platform because of constraints introduced
/// > by [`MenuItem`](tao::menu::MenuItem).
pub fn build_default_menu_bar() -> Menu {
let menu = Menu::new();
// since it is uncommon on windows to have an "application menu"
// we add a "window" menu to be more consistent across platforms with the standard menu
let window_menu = Submenu::new("Window", true);
window_menu
.append_items(&[
&PredefinedMenuItem::fullscreen(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::hide(None),
&PredefinedMenuItem::hide_others(None),
&PredefinedMenuItem::show_all(None),
&PredefinedMenuItem::maximize(None),
&PredefinedMenuItem::minimize(None),
&PredefinedMenuItem::close_window(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::quit(None),
])
.unwrap();
let edit_menu = Submenu::new("Window", true);
edit_menu
.append_items(&[
&PredefinedMenuItem::undo(None),
&PredefinedMenuItem::redo(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::cut(None),
&PredefinedMenuItem::copy(None),
&PredefinedMenuItem::paste(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::select_all(None),
])
.unwrap();
menu.append_items(&[&window_menu, &edit_menu]).unwrap();
menu
}

View file

@ -1,196 +1,22 @@
use crate::DesktopContext;
use dioxus_core::ScopeState;
use slab::Slab;
use crate::assets::*;
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::{status::StatusCode, Request, Response},
Result,
};
use crate::desktop_context::EditQueue;
static MINIFIED: &str = include_str!("./minified.js");
fn module_loader(root_name: &str, headless: bool) -> String {
format!(
r#"
<script type="module">
{MINIFIED}
// Wait for the page to load
window.onload = function() {{
let rootname = "{root_name}";
let root_element = window.document.getElementById(rootname);
if (root_element != null) {{
window.interpreter.initialize(root_element);
window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
}}
window.interpreter.wait_for_request({headless});
}}
</script>
"#
)
}
/// An arbitrary asset is an HTTP response containing a binary body.
pub type AssetResponse = Response<Cow<'static, [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 {
path: PathBuf,
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 AssetHandlerRegistryInner =
Slab<Box<dyn Fn(&AssetRequest) -> Pin<Box<dyn AssetFuture>> + Send + Sync + 'static>>;
#[derive(Clone)]
pub struct AssetHandlerRegistry(Arc<RwLock<AssetHandlerRegistryInner>>);
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 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
}
}
/// A handle to a registered asset handler.
pub struct AssetHandlerHandle {
desktop: DesktopContext,
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()
}
}
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;
}
})
});
}
}
/// Provide a callback to handle asset loading yourself.
///
/// 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>(
cx: &ScopeState,
handler: impl AssetHandler<F>,
) -> &AssetHandlerHandle {
cx.use_hook(|| {
let desktop = crate::window();
let handler_id = Rc::new(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,
}
})
}
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,
edit_queue: &crate::edits::EditQueue,
headless: bool,
responder: wry::RequestAsyncResponder,
) {
@ -293,6 +119,26 @@ pub(super) async fn desktop_handler(
}
}
fn module_loader(root_name: &str, headless: bool) -> String {
format!(
r#"
<script type="module">
{MINIFIED}
// Wait for the page to load
window.onload = function() {{
let rootname = "{root_name}";
let root_element = window.document.getElementById(rootname);
if (root_element != null) {{
window.interpreter.initialize(root_element);
window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
}}
window.interpreter.wait_for_request({headless});
}}
</script>
"#
)
}
#[allow(unreachable_code)]
fn get_asset_root() -> Option<PathBuf> {
/*

View file

@ -1,5 +1,5 @@
use crate::desktop_context::{EventData, UserWindowEvent};
use futures_util::{pin_mut, task::ArcWake, FutureExt};
use futures_util::task::ArcWake;
use std::sync::Arc;
use tao::{event_loop::EventLoopProxy, window::WindowId};
@ -8,7 +8,7 @@ use tao::{event_loop::EventLoopProxy, window::WindowId};
/// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView.
///
/// All other IO lives in the Tokio runtime,
pub fn tao_waker(proxy: &EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::task::Waker {
pub fn tao_waker(proxy: EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::task::Waker {
struct DomHandle {
proxy: EventLoopProxy<UserWindowEvent>,
id: WindowId,
@ -27,8 +27,5 @@ pub fn tao_waker(proxy: &EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::
}
}
futures_util::task::waker(Arc::new(DomHandle {
id,
proxy: proxy.clone(),
}))
futures_util::task::waker(Arc::new(DomHandle { id, proxy }))
}

View file

@ -1,26 +1,50 @@
use crate::desktop_context::{EditQueue, EventData};
use crate::protocol::{self, AssetHandlerRegistry};
use crate::{desktop_context::UserWindowEvent, Config};
use muda::{Menu, MenuItem, PredefinedMenuItem, Submenu};
use std::{rc::Rc, task::Waker};
use crate::edits::{EditQueue, WebviewQueue};
use crate::{
assets::AssetHandlerRegistry, desktop_context::UserWindowEvent, waker::tao_waker, Config,
DesktopContext,
};
use crate::{
desktop_context::{EventData, WindowEventHandlers},
eval::init_eval,
shortcut::ShortcutRegistry,
};
use crate::{
protocol::{self},
DesktopService,
};
use dioxus_core::VirtualDom;
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
use tao::window::Window;
use wry::http::Response;
use wry::{WebContext, WebView, WebViewBuilder};
use wry::{WebContext, WebViewBuilder};
pub(crate) fn build(
cfg: &mut Config,
pub struct WebviewHandler {
pub dom: VirtualDom,
pub desktop_context: DesktopContext,
pub waker: Waker,
// Wry assumes the webcontext is alive for the lifetime of the webview.
// We need to keep the webcontext alive, otherwise the webview will crash
_web_context: WebContext,
}
pub fn create_new_window(
mut cfg: Config,
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
proxy: EventLoopProxy<UserWindowEvent>,
) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue, Window) {
let mut builder = cfg.window.clone();
proxy: &EventLoopProxy<UserWindowEvent>,
dom: VirtualDom,
queue: &WebviewQueue,
event_handlers: &WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
) -> WebviewHandler {
let window = cfg.window.clone().build(event_loop).unwrap();
// TODO: restore the menu bar with muda: https://github.com/tauri-apps/muda/blob/dev/examples/wry.rs
// TODO: allow users to specify their own menubars, again :/
if cfg.enable_default_menu_bar {
// builder = builder.with_menu(build_default_menu_bar());
use crate::menubar::*;
build_menu_bar(build_default_menu_bar(), &window);
}
let window = builder.build(event_loop).unwrap();
let window_id = window.id();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
@ -49,10 +73,13 @@ pub(crate) fn build(
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
.with_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));
.with_ipc_handler({
let proxy = 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"), {
@ -101,76 +128,49 @@ pub(crate) fn build(
webview = webview.with_custom_protocol(name, move |r| handler(r))
}
const INITIALIZATION_SCRIPT: &str = r#"
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
window.event.returnValue = false;
});
}
"#;
if cfg.disable_context_menu {
// in release mode, we don't want to show the dev tool or reload menus
webview = webview.with_initialization_script(
r#"
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
window.event.returnValue = false;
});
}
"#,
)
webview = webview.with_initialization_script(INITIALIZATION_SCRIPT)
} else {
// in debug, we are okay with the reload menu showing and dev tool
webview = webview.with_devtools(true);
}
(
webview.build().unwrap(),
web_context,
asset_handlers,
edit_queue,
let webview = webview.build().unwrap();
let desktop_context = Rc::from(DesktopService::new(
window,
)
}
/// Builds a standard menu bar depending on the users platform. It may be used as a starting point
/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder).
/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste.
/// > The menu bar differs per platform because of constraints introduced
/// > by [`MenuItem`](tao::menu::MenuItem).
pub fn build_default_menu_bar() -> Menu {
let menu = Menu::new();
// since it is uncommon on windows to have an "application menu"
// we add a "window" menu to be more consistent across platforms with the standard menu
let window_menu = Submenu::new("Window", true);
window_menu
.append_items(&[
&PredefinedMenuItem::fullscreen(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::hide(None),
&PredefinedMenuItem::hide_others(None),
&PredefinedMenuItem::show_all(None),
&PredefinedMenuItem::maximize(None),
&PredefinedMenuItem::minimize(None),
&PredefinedMenuItem::close_window(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::quit(None),
])
.unwrap();
let edit_menu = Submenu::new("Window", true);
edit_menu
.append_items(&[
&PredefinedMenuItem::undo(None),
&PredefinedMenuItem::redo(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::cut(None),
&PredefinedMenuItem::copy(None),
&PredefinedMenuItem::paste(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::select_all(None),
])
.unwrap();
menu.append_items(&[&window_menu, &edit_menu]).unwrap();
menu
webview,
proxy.clone(),
event_loop.clone(),
queue.clone(),
event_handlers.clone(),
shortcut_manager,
edit_queue,
asset_handlers,
));
dom.base_scope().provide_context(desktop_context.clone());
init_eval(dom.base_scope());
WebviewHandler {
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
waker: tao_waker(proxy.clone(), desktop_context.window.id()),
desktop_context,
dom,
_web_context: web_context,
}
}