mirror of
https://github.com/DioxusLabs/dioxus
synced 2025-03-03 06:47:31 +00:00
WIP desktop binary protocol
This commit is contained in:
parent
cb148cc881
commit
2645b85533
6 changed files with 259 additions and 43 deletions
packages/desktop
|
@ -18,7 +18,7 @@ dioxus-hot-reload = { workspace = true, optional = true }
|
|||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
thiserror = { workspace = true }
|
||||
wry = { version = "0.31.0", default-features = false, features = ["protocol", "file-drop"] }
|
||||
wry = { version = "0.33.0", default-features = false, features = ["protocol", "file-drop", "tao"] }
|
||||
tracing = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
|
@ -33,6 +33,7 @@ webbrowser = "0.8.0"
|
|||
infer = "0.11.0"
|
||||
dunce = "1.0.2"
|
||||
slab = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
futures-util = { workspace = true }
|
||||
urlencoding = "2.1.2"
|
||||
|
|
|
@ -6,7 +6,6 @@ use wry::{
|
|||
application::window::{Window, WindowBuilder},
|
||||
http::{Request as HttpRequest, Response as HttpResponse},
|
||||
webview::FileDropEvent,
|
||||
Result as WryResult,
|
||||
};
|
||||
|
||||
// pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
|
||||
|
@ -42,7 +41,7 @@ type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
|||
|
||||
pub(crate) type WryProtocol = (
|
||||
String,
|
||||
Box<dyn Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static>,
|
||||
Box<dyn Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static>,
|
||||
);
|
||||
|
||||
impl Config {
|
||||
|
@ -120,7 +119,7 @@ impl Config {
|
|||
/// Set a custom protocol
|
||||
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static,
|
||||
F: Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static,
|
||||
{
|
||||
self.protocols.push((name, Box::new(handler)));
|
||||
self
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::rc::Weak;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::create_new_window;
|
||||
use crate::events::IpcMessage;
|
||||
|
@ -12,6 +14,8 @@ use dioxus_core::ScopeState;
|
|||
use dioxus_core::VirtualDom;
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use dioxus_interpreter_js::Channel;
|
||||
use rustc_hash::FxHashMap;
|
||||
use slab::Slab;
|
||||
use wry::application::event::Event;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
|
@ -32,14 +36,33 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
struct EditQueue {
|
||||
queue: Vec<Vec<u8>>,
|
||||
/// 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<wry::webview::RequestAsyncResponder>>>,
|
||||
}
|
||||
|
||||
impl EditQueue {
|
||||
fn push(&mut self, channel: &mut Channel) {
|
||||
pub fn handle_request(&self, responder: wry::webview::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, channel: &mut Channel) {
|
||||
let iter = channel.export_memory();
|
||||
self.queue.push(iter.collect());
|
||||
let bytes = iter.collect();
|
||||
let mut responder = self.responder.lock().unwrap();
|
||||
if let Some(responder) = responder.take() {
|
||||
responder.respond(wry::http::Response::new(bytes));
|
||||
} else {
|
||||
self.queue.lock().unwrap().push(bytes);
|
||||
}
|
||||
channel.reset();
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +99,9 @@ pub struct DesktopService {
|
|||
|
||||
pub(crate) shortcut_manager: ShortcutRegistry,
|
||||
|
||||
pub(crate) event_queue: Rc<RefCell<Vec<Vec<u8>>>>,
|
||||
pub(crate) edit_queue: EditQueue,
|
||||
pub(crate) templates: FxHashMap<String, u16>,
|
||||
pub(crate) max_template_count: u16,
|
||||
|
||||
pub(crate) channel: Channel,
|
||||
|
||||
|
@ -104,6 +129,7 @@ impl DesktopService {
|
|||
webviews: WebviewQueue,
|
||||
event_handlers: WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
edit_queue: EditQueue,
|
||||
) -> Self {
|
||||
Self {
|
||||
webview: Rc::new(webview),
|
||||
|
@ -113,8 +139,10 @@ impl DesktopService {
|
|||
pending_windows: webviews,
|
||||
event_handlers,
|
||||
shortcut_manager,
|
||||
event_queue: Rc::new(RefCell::new(Vec::new())),
|
||||
channel: Channel::new(),
|
||||
edit_queue,
|
||||
templates: Default::default(),
|
||||
max_template_count: 0,
|
||||
channel: Channel::default(),
|
||||
#[cfg(target_os = "ios")]
|
||||
views: Default::default(),
|
||||
}
|
||||
|
|
|
@ -27,11 +27,13 @@ pub use desktop_context::{
|
|||
};
|
||||
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
||||
use dioxus_core::*;
|
||||
use dioxus_html::MountedData;
|
||||
use dioxus_html::{event_bubbles, MountedData};
|
||||
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
||||
use dioxus_interpreter_js::Channel;
|
||||
use element::DesktopElement;
|
||||
use eval::init_eval;
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use rustc_hash::FxHashMap;
|
||||
use shortcut::ShortcutRegistry;
|
||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||
use std::cell::Cell;
|
||||
|
@ -305,7 +307,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
|
||||
view.dom.handle_event(&name, as_any, element, bubbles);
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
|
||||
// When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query
|
||||
|
@ -328,7 +330,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
|
||||
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
send_edits(view.dom.rebuild(), &view.desktop_context.webview);
|
||||
send_edits(view.dom.rebuild(), &view.desktop_context);
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "browser_open" => {
|
||||
|
@ -366,7 +368,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
view.dom.handle_event(event_name, data, id, event_bubbles);
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,7 +388,7 @@ fn create_new_window(
|
|||
event_handlers: &WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> WebviewHandler {
|
||||
let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
let (webview, web_context, edit_queue) = webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
let desktop_context = Rc::from(DesktopService::new(
|
||||
webview,
|
||||
proxy.clone(),
|
||||
|
@ -394,6 +396,7 @@ fn create_new_window(
|
|||
queue.clone(),
|
||||
event_handlers.clone(),
|
||||
shortcut_manager,
|
||||
edit_queue,
|
||||
));
|
||||
|
||||
let cx = dom.base_scope();
|
||||
|
@ -440,16 +443,149 @@ fn poll_vdom(view: &mut WebviewHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a list of mutations to the webview
|
||||
fn send_edits(edits: Mutations, webview: &WebView) {
|
||||
let serialized = serde_json::to_string(&edits).unwrap();
|
||||
fn send_edits(edits: Mutations, desktop_context: &DesktopContext) {
|
||||
if edits.edits.len() > 0 || edits.templates.len() > 0 {
|
||||
apply_edits(
|
||||
edits,
|
||||
&mut desktop_context.channel,
|
||||
&mut desktop_context.templates,
|
||||
&mut desktop_context.max_template_count,
|
||||
);
|
||||
desktop_context
|
||||
.edit_queue
|
||||
.add_edits(&mut desktop_context.channel)
|
||||
}
|
||||
}
|
||||
|
||||
// todo: use SSE and binary data to send the edits with lower overhead
|
||||
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
|
||||
fn apply_edits(
|
||||
mutations: Mutations,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &mut u16,
|
||||
) -> 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 as u16, 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)
|
||||
}
|
||||
|
||||
fn add_template(
|
||||
template: &Template<'static>,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &mut u16,
|
||||
) {
|
||||
for (idx, root) in template.roots.iter().enumerate() {
|
||||
create_template_node(channel, root);
|
||||
templates.insert(template.name.to_owned(), *max_template_count);
|
||||
}
|
||||
channel.add_templates(*max_template_count, template.roots.len() as u16);
|
||||
|
||||
*max_template_count += 1
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -5,9 +5,12 @@ use std::{
|
|||
};
|
||||
use wry::{
|
||||
http::{status::StatusCode, Request, Response},
|
||||
webview::RequestAsyncResponder,
|
||||
Result,
|
||||
};
|
||||
|
||||
use crate::desktop_context::EditQueue;
|
||||
|
||||
fn module_loader(root_name: &str) -> String {
|
||||
let js = INTERPRETER_JS.replace(
|
||||
"/*POST_HANDLE_EDITS*/",
|
||||
|
@ -53,10 +56,11 @@ fn module_loader(root_name: &str) -> String {
|
|||
|
||||
pub(super) fn desktop_handler(
|
||||
request: &Request<Vec<u8>>,
|
||||
responder: wry::http::response::Responder,
|
||||
responder: RequestAsyncResponder,
|
||||
custom_head: Option<String>,
|
||||
custom_index: Option<String>,
|
||||
root_name: &str,
|
||||
edit_queue: &EditQueue,
|
||||
) {
|
||||
// If the request is for the root, we'll serve the index.html file.
|
||||
if request.uri().path() == "/" {
|
||||
|
@ -81,15 +85,30 @@ pub(super) fn desktop_handler(
|
|||
}
|
||||
};
|
||||
|
||||
return Response::builder()
|
||||
match Response::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.body(Cow::from(body))
|
||||
.map_err(From::from);
|
||||
{
|
||||
Ok(response) => {
|
||||
responder.respond(response);
|
||||
return;
|
||||
}
|
||||
Err(err) => tracing::error!("error building response: {}", err),
|
||||
}
|
||||
} else if request.uri().path() == "/common.js" {
|
||||
return Response::builder()
|
||||
match Response::builder()
|
||||
.header("Content-Type", "text/javascript")
|
||||
.body(Cow::from(COMMON_JS.as_bytes()))
|
||||
.map_err(From::from);
|
||||
{
|
||||
Ok(response) => {
|
||||
responder.respond(response);
|
||||
return;
|
||||
}
|
||||
Err(err) => tracing::error!("error building response: {}", err),
|
||||
}
|
||||
} else if request.uri().path() == "/edits" {
|
||||
edit_queue.handle_request(responder);
|
||||
return;
|
||||
}
|
||||
|
||||
// Else, try to serve a file from the filesystem.
|
||||
|
@ -107,16 +126,42 @@ pub(super) fn desktop_handler(
|
|||
}
|
||||
|
||||
if asset.exists() {
|
||||
return Response::builder()
|
||||
.header("Content-Type", get_mime_from_path(&asset)?)
|
||||
.body(Cow::from(std::fs::read(asset)?))
|
||||
.map_err(From::from);
|
||||
let content_type = match get_mime_from_path(&asset) {
|
||||
Ok(content_type) => content_type,
|
||||
Err(err) => {
|
||||
tracing::error!("error getting mime type: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let asset = match std::fs::read(asset) {
|
||||
Ok(asset) => asset,
|
||||
Err(err) => {
|
||||
tracing::error!("error reading asset: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
match Response::builder()
|
||||
.header("Content-Type", content_type)
|
||||
.body(Cow::from(asset))
|
||||
{
|
||||
Ok(response) => {
|
||||
responder.respond(response);
|
||||
return;
|
||||
}
|
||||
Err(err) => tracing::error!("error building response: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
Response::builder()
|
||||
match Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Cow::from(String::from("Not Found").into_bytes()))
|
||||
.map_err(From::from)
|
||||
{
|
||||
Ok(response) => {
|
||||
responder.respond(response);
|
||||
return;
|
||||
}
|
||||
Err(err) => tracing::error!("error building response: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use crate::desktop_context::EventData;
|
||||
use crate::desktop_context::{EditQueue, EventData};
|
||||
use crate::protocol;
|
||||
use crate::{desktop_context::UserWindowEvent, Config};
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
|
@ -11,7 +13,7 @@ pub fn build(
|
|||
cfg: &mut Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> (WebView, WebContext) {
|
||||
) -> (WebView, WebContext, EditQueue) {
|
||||
let builder = cfg.window.clone();
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
|
@ -32,6 +34,7 @@ pub fn build(
|
|||
}
|
||||
|
||||
let mut web_context = WebContext::new(cfg.data_dir.clone());
|
||||
let edit_queue = EditQueue::default();
|
||||
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
|
@ -44,14 +47,18 @@ pub fn build(
|
|||
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
|
||||
}
|
||||
})
|
||||
.with_asynchronous_custom_protocol("dioxus".into(), move |r, responder| {
|
||||
protocol::desktop_handler(
|
||||
r,
|
||||
responder,
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
&root_name,
|
||||
)
|
||||
.with_asynchronous_custom_protocol("dioxus".into(), {
|
||||
let edit_queue = edit_queue.clone();
|
||||
move |r, responder| {
|
||||
protocol::desktop_handler(
|
||||
&r,
|
||||
responder,
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
&root_name,
|
||||
&edit_queue,
|
||||
)
|
||||
}
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
|
@ -100,5 +107,5 @@ pub fn build(
|
|||
webview = webview.with_devtools(true);
|
||||
}
|
||||
|
||||
(webview.build().unwrap(), web_context)
|
||||
(webview.build().unwrap(), web_context, edit_queue)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue