Merge branch 'master' into feat/resize-window

This commit is contained in:
Jon Kelley authored and GitHub committed 2022-12-23 11:11:11 -05:00
commit 8802747214
349 files changed
+19104 -20656

No files matched your search

+12 -2
View File
@@ -21,13 +21,14 @@ pub struct Config {
pub(crate) resource_dir: Option<PathBuf>,
pub(crate) custom_head: Option<String>,
pub(crate) custom_index: Option<String>,
pub(crate) root_name: String,
}
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
pub(crate) type WryProtocol = (
String,
Box<dyn Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static>,
Box<dyn Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Vec<u8>>> + 'static>,
);
impl Config {
@@ -46,6 +47,7 @@ impl Config {
resource_dir: None,
custom_head: None,
custom_index: None,
root_name: "main".to_string(),
}
}
@@ -96,7 +98,7 @@ impl Config {
/// Set a custom protocol
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
where
F: Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static,
F: Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Vec<u8>>> + 'static,
{
self.protocols.push((name, Box::new(handler)));
self
@@ -126,6 +128,14 @@ impl Config {
self.custom_index = Some(index);
self
}
/// Set the name of the element that Dioxus will use as the root.
///
/// This is akint to calling React.render() on the element with the specified name.
pub fn with_root_name(mut self, name: impl Into<String>) -> Self {
self.root_name = name.into();
self
}
}
impl Default for Config {
+66 -38
View File
@@ -1,10 +1,16 @@
use crate::desktop_context::{DesktopContext, UserWindowEvent};
use dioxus_core::*;
use dioxus_html::HtmlEvent;
use futures_channel::mpsc::{unbounded, UnboundedSender};
use futures_util::StreamExt;
#[cfg(target_os = "ios")]
use objc::runtime::Object;
use serde_json::Value;
use std::{
collections::HashMap,
sync::Arc,
sync::{atomic::AtomicBool, Mutex},
time::Duration,
};
use wry::{
self,
@@ -14,10 +20,15 @@ use wry::{
pub(super) struct DesktopController {
pub(super) webviews: HashMap<WindowId, WebView>,
pub(super) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender<Value>,
pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
pub(super) quit_app_on_close: bool,
pub(super) is_ready: Arc<AtomicBool>,
pub(super) proxy: EventLoopProxy<UserWindowEvent>,
pub(super) event_tx: UnboundedSender<serde_json::Value>,
#[cfg(target_os = "ios")]
pub(super) views: Vec<*mut Object>,
}
impl DesktopController {
@@ -29,64 +40,71 @@ impl DesktopController {
proxy: EventLoopProxy<UserWindowEvent>,
) -> Self {
let edit_queue = Arc::new(Mutex::new(Vec::new()));
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
let (event_tx, mut event_rx) = unbounded();
let proxy2 = proxy.clone();
let pending_edits = edit_queue.clone();
let return_sender = sender.clone();
let desktop_context_proxy = proxy.clone();
let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::<Value>();
std::thread::spawn(move || {
// We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
// We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
// I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
let mut dom =
VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver));
let window_context = DesktopContext::new(desktop_context_proxy);
dom.base_scope().provide_context(window_context);
// allow other proccesses to send the new rsx text to the @dioxusin ipc channel and recieve erros on the @dioxusout channel
#[cfg(any(feature = "hot-reload", debug_assertions))]
crate::hot_reload::init(&dom);
let edits = dom.rebuild();
edit_queue
.lock()
.unwrap()
.push(serde_json::to_string(&edits.edits).unwrap());
// Make sure the window is ready for any new updates
proxy.send_event(UserWindowEvent::Update).unwrap();
let mut dom = VirtualDom::new_with_props(root, props)
.with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever));
{
let edits = dom.rebuild();
let mut queue = edit_queue.lock().unwrap();
queue.push(serde_json::to_string(&edits).unwrap());
proxy.send_event(UserWindowEvent::EditsReady).unwrap();
}
loop {
dom.wait_for_work().await;
let muts = dom.work_with_deadline(|| false);
for edit in muts {
edit_queue
.lock()
.unwrap()
.push(serde_json::to_string(&edit.edits).unwrap());
tokio::select! {
_ = dom.wait_for_work() => {}
Some(json_value) = event_rx.next() => {
if let Ok(value) = serde_json::from_value::<HtmlEvent>(json_value) {
let HtmlEvent {
name,
element,
bubbles,
data
} = value;
dom.handle_event(&name, data.into_any(), element, bubbles);
}
}
}
let _ = proxy.send_event(UserWindowEvent::Update);
let muts = dom
.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)))
.await;
edit_queue
.lock()
.unwrap()
.push(serde_json::to_string(&muts).unwrap());
let _ = proxy.send_event(UserWindowEvent::EditsReady);
}
})
});
Self {
pending_edits,
sender: return_sender,
eval_sender,
webviews: HashMap::new(),
is_ready: Arc::new(AtomicBool::new(false)),
quit_app_on_close: true,
proxy: proxy2,
event_tx,
#[cfg(target_os = "ios")]
views: vec![],
}
}
@@ -100,13 +118,23 @@ impl DesktopController {
pub(super) fn try_load_ready_webviews(&mut self) {
if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
let mut queue = self.pending_edits.lock().unwrap();
let mut new_queue = Vec::new();
{
let mut queue = self.pending_edits.lock().unwrap();
std::mem::swap(&mut new_queue, &mut *queue);
}
let (_id, view) = self.webviews.iter_mut().next().unwrap();
for edit in queue.drain(..) {
for edit in new_queue.drain(..) {
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
.unwrap();
}
}
}
pub(crate) fn set_template(&self, _serialized_template: String) {
todo!("hot reloading currently WIP")
}
}
+175 -59
View File
@@ -1,8 +1,17 @@
use std::rc::Rc;
use crate::controller::DesktopController;
use dioxus_core::ScopeState;
use wry::application::dpi::LogicalSize;
use serde::de::Error;
use serde_json::Value;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
use wry::application::event_loop::ControlFlow;
use wry::application::event_loop::EventLoopProxy;
#[cfg(target_os = "ios")]
use wry::application::platform::ios::WindowExtIOS;
use wry::application::window::Fullscreen as WryFullscreen;
use UserWindowEvent::*;
@@ -32,11 +41,18 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
pub struct DesktopContext {
/// The wry/tao proxy to the current window
pub proxy: ProxyType,
pub(super) eval_reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Value>>>,
}
impl DesktopContext {
pub(crate) fn new(proxy: ProxyType) -> Self {
Self { proxy }
pub(crate) fn new(
proxy: ProxyType,
eval_reciever: tokio::sync::mpsc::UnboundedReceiver<Value>,
) -> Self {
Self {
proxy,
eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)),
}
}
/// trigger the drag-window event
@@ -140,16 +156,34 @@ impl DesktopContext {
pub fn eval(&self, script: impl std::string::ToString) {
let _ = self.proxy.send_event(Eval(script.to_string()));
}
/// Push view
#[cfg(target_os = "ios")]
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
let _ = self.proxy.send_event(PushView(view));
}
/// Push view
#[cfg(target_os = "ios")]
pub fn pop_view(&self) {
let _ = self.proxy.send_event(PopView);
}
}
#[derive(Debug)]
pub enum UserWindowEvent {
Update,
EditsReady,
Initialize,
CloseWindow,
DragWindow,
FocusWindow,
/// Set a new Dioxus template for hot-reloading
///
/// Is a no-op in release builds. Must fit the right format for templates
SetTemplate(String),
Visible(bool),
Minimize(bool),
Maximize(bool),
@@ -171,74 +205,156 @@ pub enum UserWindowEvent {
DevTool,
Eval(String),
#[cfg(target_os = "ios")]
PushView(objc_id::ShareId<objc::runtime::Object>),
#[cfg(target_os = "ios")]
PopView,
}
pub(super) fn handler(
user_event: UserWindowEvent,
desktop: &mut DesktopController,
control_flow: &mut ControlFlow,
) {
// currently dioxus-desktop supports a single window only,
// so we can grab the only webview from the map;
let webview = desktop.webviews.values().next().unwrap();
let window = webview.window();
impl DesktopController {
pub(super) fn handle_event(
&mut self,
user_event: UserWindowEvent,
control_flow: &mut ControlFlow,
) {
// currently dioxus-desktop supports a single window only,
// so we can grab the only webview from the map;
// on wayland it is possible that a user event is emitted
// before the webview is initialized. ignore the event.
let webview = if let Some(webview) = self.webviews.values().next() {
webview
} else {
return;
};
match user_event {
Update => desktop.try_load_ready_webviews(),
CloseWindow => *control_flow = ControlFlow::Exit,
DragWindow => {
// if the drag_window has any errors, we don't do anything
window.fullscreen().is_none().then(|| window.drag_window());
}
Visible(state) => window.set_visible(state),
Minimize(state) => window.set_minimized(state),
Maximize(state) => window.set_maximized(state),
MaximizeToggle => window.set_maximized(!window.is_maximized()),
Fullscreen(state) => {
if let Some(handle) = window.current_monitor() {
window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle))));
let window = webview.window();
match user_event {
Initialize | EditsReady => self.try_load_ready_webviews(),
SetTemplate(template) => self.set_template(template),
CloseWindow => *control_flow = ControlFlow::Exit,
DragWindow => {
// if the drag_window has any errors, we don't do anything
window.fullscreen().is_none().then(|| window.drag_window());
}
}
FocusWindow => window.set_focus(),
Resizable(state) => window.set_resizable(state),
AlwaysOnTop(state) => window.set_always_on_top(state),
CursorVisible(state) => window.set_cursor_visible(state),
CursorGrab(state) => {
let _ = window.set_cursor_grab(state);
}
SetTitle(content) => window.set_title(&content),
SetDecorations(state) => window.set_decorations(state),
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
SetInnerSize(logical_size) => window.set_inner_size(logical_size),
Print => {
if let Err(e) = webview.print() {
// we can't panic this error.
log::warn!("Open print modal failed: {e}");
Visible(state) => window.set_visible(state),
Minimize(state) => window.set_minimized(state),
Maximize(state) => window.set_maximized(state),
MaximizeToggle => window.set_maximized(!window.is_maximized()),
Fullscreen(state) => {
if let Some(handle) = window.current_monitor() {
window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle))));
}
}
}
DevTool => {
#[cfg(debug_assertions)]
webview.open_devtools();
#[cfg(not(debug_assertions))]
log::warn!("Devtools are disabled in release builds");
}
FocusWindow => window.set_focus(),
Resizable(state) => window.set_resizable(state),
AlwaysOnTop(state) => window.set_always_on_top(state),
Eval(code) => {
if let Err(e) = webview.evaluate_script(code.as_str()) {
// we can't panic this error.
log::warn!("Eval script error: {e}");
Eval(code) => {
let script = format!(
r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{
{}
}})()}}));"#,
code
);
if let Err(e) = webview.evaluate_script(&script) {
// we can't panic this error.
log::warn!("Eval script error: {e}");
}
}
CursorVisible(state) => window.set_cursor_visible(state),
CursorGrab(state) => {
let _ = window.set_cursor_grab(state);
}
SetTitle(content) => window.set_title(&content),
SetDecorations(state) => window.set_decorations(state),
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
SetInnerSize(logical_size) => window.set_inner_size(logical_size),
Print => {
if let Err(e) = webview.print() {
// we can't panic this error.
log::warn!("Open print modal failed: {e}");
}
}
DevTool => {
#[cfg(debug_assertions)]
webview.open_devtools();
#[cfg(not(debug_assertions))]
log::warn!("Devtools are disabled in release builds");
}
#[cfg(target_os = "ios")]
PushView(view) => unsafe {
use objc::runtime::Object;
use objc::*;
assert!(is_main_thread());
let ui_view = window.ui_view() as *mut Object;
let ui_view_frame: *mut Object = msg_send![ui_view, frame];
let _: () = msg_send![view, setFrame: ui_view_frame];
let _: () = msg_send![view, setAutoresizingMask: 31];
let ui_view_controller = window.ui_view_controller() as *mut Object;
let _: () = msg_send![ui_view_controller, setView: view];
self.views.push(ui_view);
},
#[cfg(target_os = "ios")]
PopView => unsafe {
use objc::runtime::Object;
use objc::*;
assert!(is_main_thread());
if let Some(view) = self.views.pop() {
let ui_view_controller = window.ui_view_controller() as *mut Object;
let _: () = msg_send![ui_view_controller, setView: view];
}
},
}
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) {
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult {
let desktop = use_window(cx).clone();
cx.use_hook(|| {
move |script| {
desktop.eval(script);
let recv = desktop.eval_reciever.clone();
EvalResult { reciever: recv }
}
})
}
cx.use_hook(|| move |script| desktop.eval(script))
/// A future that resolves to the result of a JavaScript evaluation.
pub struct EvalResult {
reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>>,
}
impl IntoFuture for EvalResult {
type Output = Result<serde_json::Value, serde_json::Error>;
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut reciever = self.reciever.lock().await;
match reciever.recv().await {
Some(result) => Ok(result),
None => Err(serde_json::Error::custom("No result returned")),
}
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
}
}
#[cfg(target_os = "ios")]
fn is_main_thread() -> bool {
use objc::runtime::{Class, BOOL, NO};
use objc::*;
let cls = Class::get("NSThread").unwrap();
let result: BOOL = unsafe { msg_send![cls, isMainThread] };
result != NO
}
-192
View File
@@ -1,12 +1,5 @@
//! Convert a serialized event to an event trigger
use std::any::Any;
use std::sync::Arc;
use dioxus_core::ElementId;
use dioxus_core::{EventPriority, UserEvent};
use dioxus_html::event_bubbles;
use dioxus_html::on::*;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
@@ -34,188 +27,3 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
}
}
}
#[derive(Deserialize, Serialize)]
struct ImEvent {
event: String,
mounted_dom_id: ElementId,
contents: serde_json::Value,
}
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
let ImEvent {
event,
mounted_dom_id,
contents,
} = serde_json::from_value(val).unwrap();
let mounted_dom_id = Some(mounted_dom_id);
let name = event_name_from_type(&event);
let event = make_synthetic_event(&event, contents);
UserEvent {
name,
priority: EventPriority::Low,
scope_id: None,
element: mounted_dom_id,
bubbles: event_bubbles(name),
data: event,
}
}
fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Send + Sync> {
match name {
"copy" | "cut" | "paste" => {
//
Arc::new(ClipboardData {})
}
"compositionend" | "compositionstart" | "compositionupdate" => {
Arc::new(serde_json::from_value::<CompositionData>(val).unwrap())
}
"keydown" | "keypress" | "keyup" => {
let evt = serde_json::from_value::<KeyboardData>(val).unwrap();
Arc::new(evt)
}
"focus" | "blur" | "focusout" | "focusin" => {
//
Arc::new(FocusData {})
}
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
// don't have a good solution with the serialized event problem
"change" | "input" | "invalid" | "reset" | "submit" => {
Arc::new(serde_json::from_value::<FormData>(val).unwrap())
}
"click" | "contextmenu" | "dblclick" | "doubleclick" | "drag" | "dragend" | "dragenter"
| "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown"
| "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
Arc::new(serde_json::from_value::<MouseData>(val).unwrap())
}
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
Arc::new(serde_json::from_value::<PointerData>(val).unwrap())
}
"select" => {
//
Arc::new(serde_json::from_value::<SelectionData>(val).unwrap())
}
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
Arc::new(serde_json::from_value::<TouchData>(val).unwrap())
}
"scroll" => Arc::new(ScrollData {}),
"wheel" => Arc::new(serde_json::from_value::<WheelData>(val).unwrap()),
"animationstart" | "animationend" | "animationiteration" => {
Arc::new(serde_json::from_value::<AnimationData>(val).unwrap())
}
"transitionend" => Arc::new(serde_json::from_value::<TransitionData>(val).unwrap()),
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => {
//
Arc::new(MediaData {})
}
"toggle" => Arc::new(ToggleData {}),
_ => Arc::new(()),
}
}
fn event_name_from_type(typ: &str) -> &'static str {
match typ {
"copy" => "copy",
"cut" => "cut",
"paste" => "paste",
"compositionend" => "compositionend",
"compositionstart" => "compositionstart",
"compositionupdate" => "compositionupdate",
"keydown" => "keydown",
"keypress" => "keypress",
"keyup" => "keyup",
"focus" => "focus",
"focusout" => "focusout",
"focusin" => "focusin",
"blur" => "blur",
"change" => "change",
"input" => "input",
"invalid" => "invalid",
"reset" => "reset",
"submit" => "submit",
"click" => "click",
"contextmenu" => "contextmenu",
"doubleclick" => "doubleclick",
"dblclick" => "dblclick",
"drag" => "drag",
"dragend" => "dragend",
"dragenter" => "dragenter",
"dragexit" => "dragexit",
"dragleave" => "dragleave",
"dragover" => "dragover",
"dragstart" => "dragstart",
"drop" => "drop",
"mousedown" => "mousedown",
"mouseenter" => "mouseenter",
"mouseleave" => "mouseleave",
"mousemove" => "mousemove",
"mouseout" => "mouseout",
"mouseover" => "mouseover",
"mouseup" => "mouseup",
"pointerdown" => "pointerdown",
"pointermove" => "pointermove",
"pointerup" => "pointerup",
"pointercancel" => "pointercancel",
"gotpointercapture" => "gotpointercapture",
"lostpointercapture" => "lostpointercapture",
"pointerenter" => "pointerenter",
"pointerleave" => "pointerleave",
"pointerover" => "pointerover",
"pointerout" => "pointerout",
"select" => "select",
"touchcancel" => "touchcancel",
"touchend" => "touchend",
"touchmove" => "touchmove",
"touchstart" => "touchstart",
"scroll" => "scroll",
"wheel" => "wheel",
"animationstart" => "animationstart",
"animationend" => "animationend",
"animationiteration" => "animationiteration",
"transitionend" => "transitionend",
"abort" => "abort",
"canplay" => "canplay",
"canplaythrough" => "canplaythrough",
"durationchange" => "durationchange",
"emptied" => "emptied",
"encrypted" => "encrypted",
"ended" => "ended",
"error" => "error",
"loadeddata" => "loadeddata",
"loadedmetadata" => "loadedmetadata",
"loadstart" => "loadstart",
"pause" => "pause",
"play" => "play",
"playing" => "playing",
"progress" => "progress",
"ratechange" => "ratechange",
"seeked" => "seeked",
"seeking" => "seeking",
"stalled" => "stalled",
"suspend" => "suspend",
"timeupdate" => "timeupdate",
"volumechange" => "volumechange",
"waiting" => "waiting",
"toggle" => "toggle",
a => {
panic!("unsupported event type {:?}", a);
}
}
}
+14 -9
View File
@@ -1,4 +1,7 @@
use dioxus_core::{SchedulerMsg, SetTemplateMsg, VirtualDom};
#![allow(dead_code)]
use dioxus_core::VirtualDom;
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
use std::io::{BufRead, BufReader};
use std::time::Duration;
@@ -10,32 +13,34 @@ fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalS
.ok()
}
pub(crate) fn init(dom: &VirtualDom) {
pub(crate) fn init(_dom: &VirtualDom) {
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
Arc::new(Mutex::new(None));
let latest_in_connection_handle = latest_in_connection.clone();
// connect to processes for incoming data
std::thread::spawn(move || {
if let Ok(listener) = LocalSocketListener::bind("@dioxusin") {
let temp_file = std::env::temp_dir().join("@dioxusin");
if let Ok(listener) = LocalSocketListener::bind(temp_file) {
for conn in listener.incoming().filter_map(handle_error) {
*latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
}
}
});
let mut channel = dom.get_scheduler_channel();
std::thread::spawn(move || {
loop {
if let Some(conn) = &mut *latest_in_connection.lock().unwrap() {
let mut buf = String::new();
match conn.read_line(&mut buf) {
Ok(_) => {
let msg: SetTemplateMsg = serde_json::from_str(&buf).unwrap();
channel
.start_send(SchedulerMsg::SetTemplate(Box::new(msg)))
.unwrap();
todo!()
// let msg: SetTemplateMsg = serde_json::from_str(&buf).unwrap();
// channel
// .start_send(SchedulerMsg::SetTemplate(Box::new(msg)))
// .unwrap();
}
Err(err) => {
if err.kind() != std::io::ErrorKind::WouldBlock {
+140 -111
View File
@@ -8,16 +8,20 @@ mod controller;
mod desktop_context;
mod escape;
mod events;
#[cfg(any(feature = "hot-reload", debug_assertions))]
mod hot_reload;
mod protocol;
#[cfg(all(feature = "hot-reload", debug_assertions))]
mod hot_reload;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use desktop_context::UserWindowEvent;
pub use desktop_context::{use_eval, use_window, DesktopContext};
pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult};
use futures_channel::mpsc::UnboundedSender;
pub use wry;
pub use wry::application as tao;
use crate::events::trigger_from_serialized;
pub use cfg::Config;
use controller::DesktopController;
use dioxus_core::*;
@@ -100,99 +104,145 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
/// ```
pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cfg: Config) {
let event_loop = EventLoop::with_user_event();
let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy());
let proxy = event_loop.create_proxy();
// We assume that if the icon is None, then the user just didnt set it
event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
match window_event {
Event::NewEvents(StartCause::Init) => desktop.start(&mut cfg, event_loop),
Event::WindowEvent {
event, window_id, ..
} => match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
_ => {}
},
Event::UserEvent(user_event) => desktop.handle_event(user_event, control_flow),
Event::MainEventsCleared => {}
Event::Resumed => {}
Event::Suspended => {}
Event::LoopDestroyed => {}
Event::RedrawRequested(_id) => {}
_ => {}
}
})
}
impl DesktopController {
fn start(
&mut self,
cfg: &mut Config,
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
) {
let webview = build_webview(
cfg,
event_loop,
self.is_ready.clone(),
self.proxy.clone(),
self.eval_sender.clone(),
self.event_tx.clone(),
);
self.webviews.insert(webview.window().id(), webview);
}
}
fn build_webview(
cfg: &mut Config,
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
is_ready: Arc<AtomicBool>,
proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
eval_sender: tokio::sync::mpsc::UnboundedSender<serde_json::Value>,
event_tx: UnboundedSender<serde_json::Value>,
) -> wry::webview::WebView {
let builder = cfg.window.clone();
let window = builder.build(event_loop).unwrap();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
let resource_dir = cfg.resource_dir.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() {
cfg.window.window.window_icon = Some(
window.set_window_icon(Some(
tao::window::Icon::from_rgba(
include_bytes!("./assets/default_icon.bin").to_vec(),
460,
460,
)
.expect("image parse failed"),
);
));
}
event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
match window_event {
Event::NewEvents(StartCause::Init) => {
let builder = cfg.window.clone();
let window = builder.build(event_loop).unwrap();
let window_id = window.id();
let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone());
let proxy = proxy.clone();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
let resource_dir = cfg.resource_dir.clone();
let index_file = cfg.custom_index.clone();
let mut webview = WebViewBuilder::new(window)
.unwrap()
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
.with_ipc_handler(move |_window: &Window, payload: String| {
parse_ipc_message(&payload)
.map(|message| match message.method() {
"user_event" => {
let event = trigger_from_serialized(message.params());
log::trace!("User event: {:?}", event);
sender.unbounded_send(SchedulerMsg::Event(event)).unwrap();
}
"initialize" => {
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
let _ = proxy.send_event(UserWindowEvent::Update);
}
"browser_open" => {
let data = message.params();
log::trace!("Open browser: {:?}", data);
if let Some(temp) = data.as_object() {
if temp.contains_key("href") {
let url = temp.get("href").unwrap().as_str().unwrap();
if let Err(e) = webbrowser::open(url) {
log::error!("Open Browser error: {:?}", e);
}
}
}
}
_ => (),
})
.unwrap_or_else(|| {
log::warn!("invalid IPC message received");
});
})
.with_custom_protocol(String::from("dioxus"), move |r| {
protocol::desktop_handler(
r,
resource_dir.clone(),
custom_head.clone(),
index_file.clone(),
)
})
.with_file_drop_handler(move |window, evet| {
file_handler
.as_ref()
.map(|handler| handler(window, evet))
.unwrap_or_default()
});
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler)
let mut webview = WebViewBuilder::new(window)
.unwrap()
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
.with_ipc_handler(move |_window: &Window, payload: String| {
let message = match parse_ipc_message(&payload) {
Some(message) => message,
None => {
log::error!("Failed to parse IPC message: {}", payload);
return;
}
};
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#"
match message.method() {
"eval_result" => {
let result = message.params();
eval_sender.send(result).unwrap();
}
"user_event" => {
_ = event_tx.unbounded_send(message.params());
}
"initialize" => {
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
let _ = proxy.send_event(UserWindowEvent::EditsReady);
}
"browser_open" => {
let data = message.params();
log::trace!("Open browser: {:?}", data);
if let Some(temp) = data.as_object() {
if temp.contains_key("href") {
let url = temp.get("href").unwrap().as_str().unwrap();
if let Err(e) = webbrowser::open(url) {
log::error!("Open Browser error: {:?}", e);
}
}
}
}
_ => (),
}
})
.with_custom_protocol(String::from("dioxus"), move |r| {
protocol::desktop_handler(
r,
resource_dir.clone(),
custom_head.clone(),
index_file.clone(),
&root_name,
)
})
.with_file_drop_handler(move |window, evet| {
file_handler
.as_ref()
.map(|handler| handler(window, evet))
.unwrap_or_default()
});
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler)
}
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();
@@ -203,32 +253,11 @@ pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cf
});
}
"#,
)
} else {
// in debug, we are okay with the reload menu showing and dev tool
webview = webview.with_devtools(true);
}
)
} else {
// in debug, we are okay with the reload menu showing and dev tool
webview = webview.with_devtools(true);
}
desktop.webviews.insert(window_id, webview.build().unwrap());
}
Event::WindowEvent {
event, window_id, ..
} => match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
_ => {}
},
Event::UserEvent(user_event) => {
desktop_context::handler(user_event, &mut desktop, control_flow)
}
Event::MainEventsCleared => {}
Event::Resumed => {}
Event::Suspended => {}
Event::LoopDestroyed => {}
Event::RedrawRequested(_id) => {}
_ => {}
}
})
webview.build().unwrap()
}
+46 -26
View File
@@ -1,26 +1,38 @@
use dioxus_interpreter_js::INTERPRETER_JS;
use std::path::{Path, PathBuf};
use wry::{
http::{status::StatusCode, Request, Response, ResponseBuilder},
http::{status::StatusCode, Request, Response},
Result,
};
const MODULE_LOADER: &str = r#"
fn module_loader(root_name: &str) -> String {
format!(
r#"
<script>
import("./index.js").then(function (module) {
module.main();
});
{INTERPRETER_JS}
let rootname = "{}";
let root = window.document.getElementById(rootname);
if (root != null) {{
window.interpreter = new Interpreter(root);
window.ipc.postMessage(serializeIpcMessage("initialize"));
}}
</script>
"#;
"#,
root_name
)
}
pub(super) fn desktop_handler(
request: &Request,
request: &Request<Vec<u8>>,
asset_root: Option<PathBuf>,
custom_head: Option<String>,
custom_index: Option<String>,
) -> Result<Response> {
root_name: &str,
) -> Result<Response<Vec<u8>>> {
// Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case".
// For now, we only serve two pieces of content which get included as bytes into the final binary.
let path = request.uri().replace("dioxus://", "");
let path = request.uri().to_string().replace("dioxus://", "");
// all assets should be called from index.html
let trimmed = path.trim_start_matches("index.html/");
@@ -30,25 +42,30 @@ pub(super) fn desktop_handler(
// we'll look for the closing </body> tag and insert our little module loader there.
if let Some(custom_index) = custom_index {
let rendered = custom_index
.replace("</body>", &format!("{}</body>", MODULE_LOADER))
.replace("</body>", &format!("{}</body>", module_loader(root_name)))
.into_bytes();
ResponseBuilder::new().mimetype("text/html").body(rendered)
Response::builder()
.header("Content-Type", "text/html")
.body(rendered)
.map_err(From::from)
} else {
// Otherwise, we'll serve the default index.html and apply a custom head if that's specified.
let mut template = include_str!("./index.html").to_string();
if let Some(custom_head) = custom_head {
template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
}
template = template.replace("<!-- MODULE LOADER -->", MODULE_LOADER);
template = template.replace("<!-- MODULE LOADER -->", &module_loader(root_name));
ResponseBuilder::new()
.mimetype("text/html")
Response::builder()
.header("Content-Type", "text/html")
.body(template.into_bytes())
.map_err(From::from)
}
} else if trimmed == "index.js" {
ResponseBuilder::new()
.mimetype("text/javascript")
Response::builder()
.header("Content-Type", "text/javascript")
.body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec())
.map_err(From::from)
} else {
let asset_root = asset_root
.unwrap_or_else(|| get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf()))
@@ -57,20 +74,23 @@ pub(super) fn desktop_handler(
let asset = asset_root.join(trimmed).canonicalize()?;
if !asset.starts_with(asset_root) {
return ResponseBuilder::new()
return Response::builder()
.status(StatusCode::FORBIDDEN)
.body(String::from("Forbidden").into_bytes());
.body(String::from("Forbidden").into_bytes())
.map_err(From::from);
}
if !asset.exists() {
return ResponseBuilder::new()
return Response::builder()
.status(StatusCode::NOT_FOUND)
.body(String::from("Not Found").into_bytes());
.body(String::from("Not Found").into_bytes())
.map_err(From::from);
}
ResponseBuilder::new()
.mimetype(get_mime_from_path(trimmed)?)
Response::builder()
.header("Content-Type", get_mime_from_path(trimmed)?)
.body(std::fs::read(asset)?)
.map_err(From::from)
}
}
@@ -96,10 +116,10 @@ fn get_asset_root() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
let bundle = core_foundation::bundle::CFBundle::main_bundle();
let bundle_path = dbg!(bundle.path()?);
let resources_path = dbg!(bundle.resources_path()?);
let absolute_resources_root = dbg!(bundle_path.join(resources_path));
let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?);
let bundle_path = bundle.path()?;
let resources_path = bundle.resources_path()?;
let absolute_resources_root = bundle_path.join(resources_path);
let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
return Some(canonical_resources_root);
}