Merge branch 'master' into add-file-data-drag-event
This commit is contained in:
commit
7577019946
374 files changed
+28686
-1919
No files matched your search
+34
-18
@@ -3,40 +3,42 @@ name = "dioxus-desktop"
|
||||
version = "0.3.0"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Dioxus VirtualDOM renderer for a remote webview instance"
|
||||
description = "WebView renderer for Dioxus"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
homepage = "https://dioxuslabs.com/docs/0.3/guide/en/getting_started/desktop.html"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", features = ["serialize", "native-bind"], version = "^0.3.0" }
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
|
||||
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-html = { workspace = true, features = ["serialize", "native-bind"] }
|
||||
dioxus-interpreter-js = { workspace = true }
|
||||
dioxus-hot-reload = { workspace = true, optional = true }
|
||||
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
thiserror = "1.0.30"
|
||||
log = "0.4.14"
|
||||
wry = { version = "0.27.2" }
|
||||
futures-channel = "0.3.21"
|
||||
tokio = { version = "1.27", features = [
|
||||
log = { workspace = true }
|
||||
wry = { version = "0.28.0" }
|
||||
futures-channel = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"rt",
|
||||
"time",
|
||||
"macros",
|
||||
"fs",
|
||||
], optional = true, default-features = false }
|
||||
], optional = true }
|
||||
webbrowser = "0.8.0"
|
||||
infer = "0.11.0"
|
||||
dunce = "1.0.2"
|
||||
slab = "0.4"
|
||||
slab = { workspace = true }
|
||||
|
||||
futures-util = "0.3.25"
|
||||
futures-util = { workspace = true }
|
||||
urlencoding = "2.1.2"
|
||||
|
||||
|
||||
[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
|
||||
rfd = "0.11.3"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
@@ -45,6 +47,7 @@ objc_id = "0.1.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9.3"
|
||||
objc = "0.2.7"
|
||||
|
||||
[features]
|
||||
default = ["tokio_runtime", "hot-reload"]
|
||||
@@ -55,6 +58,19 @@ tray = ["wry/tray"]
|
||||
hot-reload = ["dioxus-hot-reload"]
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
dioxus-hooks = { path = "../hooks" }
|
||||
# image = "0.24.0" # enable this when generating a new desktop image
|
||||
dioxus-core-macro = { workspace = true }
|
||||
dioxus-hooks = { workspace = true }
|
||||
dioxus = { workspace = true }
|
||||
exitcode = "1.1.2"
|
||||
scraper = "0.16.0"
|
||||
|
||||
# These tests need to be run on the main thread, so they cannot use rust's test harness.
|
||||
[[test]]
|
||||
name = "check_events"
|
||||
path = "headless_tests/events.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "check_rendering"
|
||||
path = "headless_tests/rendering.rs"
|
||||
harness = false
|
||||
@@ -0,0 +1,352 @@
|
||||
use dioxus::html::geometry::euclid::Vector3D;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::DesktopContext;
|
||||
|
||||
pub(crate) fn check_app_exits(app: Component) {
|
||||
use dioxus_desktop::tao::window::WindowBuilder;
|
||||
use dioxus_desktop::Config;
|
||||
// This is a deadman's switch to ensure that the app exits
|
||||
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||
let should_panic_clone = should_panic.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_secs(100));
|
||||
if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
std::process::exit(exitcode::SOFTWARE);
|
||||
}
|
||||
});
|
||||
|
||||
dioxus_desktop::launch_cfg(
|
||||
app,
|
||||
Config::new().with_window(WindowBuilder::new().with_visible(false)),
|
||||
);
|
||||
|
||||
// Stop deadman's switch
|
||||
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
check_app_exits(app);
|
||||
}
|
||||
|
||||
fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) {
|
||||
use_effect(cx, (), |_| {
|
||||
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
||||
async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
desktop_context.eval(&format!(
|
||||
r#"let element = document.getElementById('{}');
|
||||
// Dispatch a synthetic event
|
||||
const event = {};
|
||||
console.log(element, event);
|
||||
element.dispatchEvent(event);
|
||||
"#,
|
||||
id, value
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
fn app(cx: Scope) -> Element {
|
||||
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
||||
let recieved_events = use_state(cx, || 0);
|
||||
|
||||
// button
|
||||
mock_event(
|
||||
cx,
|
||||
"button",
|
||||
r#"new MouseEvent("click", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
button: 0,
|
||||
})"#,
|
||||
);
|
||||
// mouse_move_div
|
||||
mock_event(
|
||||
cx,
|
||||
"mouse_move_div",
|
||||
r#"new MouseEvent("mousemove", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
buttons: 2,
|
||||
})"#,
|
||||
);
|
||||
// mouse_click_div
|
||||
mock_event(
|
||||
cx,
|
||||
"mouse_click_div",
|
||||
r#"new MouseEvent("click", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
buttons: 2,
|
||||
button: 2,
|
||||
})"#,
|
||||
);
|
||||
// mouse_dblclick_div
|
||||
mock_event(
|
||||
cx,
|
||||
"mouse_dblclick_div",
|
||||
r#"new MouseEvent("dblclick", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
buttons: 1|2,
|
||||
button: 2,
|
||||
})"#,
|
||||
);
|
||||
// mouse_down_div
|
||||
mock_event(
|
||||
cx,
|
||||
"mouse_down_div",
|
||||
r#"new MouseEvent("mousedown", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
buttons: 2,
|
||||
button: 2,
|
||||
})"#,
|
||||
);
|
||||
// mouse_up_div
|
||||
mock_event(
|
||||
cx,
|
||||
"mouse_up_div",
|
||||
r#"new MouseEvent("mouseup", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
buttons: 0,
|
||||
button: 0,
|
||||
})"#,
|
||||
);
|
||||
// wheel_div
|
||||
mock_event(
|
||||
cx,
|
||||
"wheel_div",
|
||||
r#"new WheelEvent("wheel", {
|
||||
view: window,
|
||||
deltaX: 1.0,
|
||||
deltaY: 2.0,
|
||||
deltaZ: 3.0,
|
||||
deltaMode: 0x00,
|
||||
bubbles: true,
|
||||
})"#,
|
||||
);
|
||||
// key_down_div
|
||||
mock_event(
|
||||
cx,
|
||||
"key_down_div",
|
||||
r#"new KeyboardEvent("keydown", {
|
||||
key: "a",
|
||||
code: "KeyA",
|
||||
location: 0,
|
||||
repeat: true,
|
||||
keyCode: 65,
|
||||
charCode: 97,
|
||||
char: "a",
|
||||
charCode: 0,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
isComposing: false,
|
||||
which: 65,
|
||||
bubbles: true,
|
||||
})"#,
|
||||
);
|
||||
// key_up_div
|
||||
mock_event(
|
||||
cx,
|
||||
"key_up_div",
|
||||
r#"new KeyboardEvent("keyup", {
|
||||
key: "a",
|
||||
code: "KeyA",
|
||||
location: 0,
|
||||
repeat: false,
|
||||
keyCode: 65,
|
||||
charCode: 97,
|
||||
char: "a",
|
||||
charCode: 0,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
isComposing: false,
|
||||
which: 65,
|
||||
bubbles: true,
|
||||
})"#,
|
||||
);
|
||||
// key_press_div
|
||||
mock_event(
|
||||
cx,
|
||||
"key_press_div",
|
||||
r#"new KeyboardEvent("keypress", {
|
||||
key: "a",
|
||||
code: "KeyA",
|
||||
location: 0,
|
||||
repeat: false,
|
||||
keyCode: 65,
|
||||
charCode: 97,
|
||||
char: "a",
|
||||
charCode: 0,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
isComposing: false,
|
||||
which: 65,
|
||||
bubbles: true,
|
||||
})"#,
|
||||
);
|
||||
// focus_in_div
|
||||
mock_event(
|
||||
cx,
|
||||
"focus_in_div",
|
||||
r#"new FocusEvent("focusin", {bubbles: true})"#,
|
||||
);
|
||||
// focus_out_div
|
||||
mock_event(
|
||||
cx,
|
||||
"focus_out_div",
|
||||
r#"new FocusEvent("focusout",{bubbles: true})"#,
|
||||
);
|
||||
|
||||
if **recieved_events == 12 {
|
||||
println!("all events recieved");
|
||||
desktop_context.close();
|
||||
}
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
button {
|
||||
id: "button",
|
||||
onclick: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().is_empty());
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
},
|
||||
}
|
||||
div {
|
||||
id: "mouse_move_div",
|
||||
onmousemove: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
},
|
||||
}
|
||||
div {
|
||||
id: "mouse_click_div",
|
||||
onclick: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
},
|
||||
}
|
||||
div{
|
||||
id: "mouse_dblclick_div",
|
||||
ondblclick: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div{
|
||||
id: "mouse_down_div",
|
||||
onmousedown: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div{
|
||||
id: "mouse_up_div",
|
||||
onmouseup: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().is_empty());
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div{
|
||||
id: "wheel_div",
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
background_color: "red",
|
||||
onwheel: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{
|
||||
panic!("Expected delta to be in pixels")
|
||||
};
|
||||
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
id: "key_down_div",
|
||||
onkeydown: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert_eq!(event.data.key().to_string(), "a");
|
||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location, 0);
|
||||
assert!(event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
id: "key_up_div",
|
||||
onkeyup: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert_eq!(event.data.key().to_string(), "a");
|
||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location, 0);
|
||||
assert!(!event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
id: "key_press_div",
|
||||
onkeypress: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert_eq!(event.data.key().to_string(), "a");
|
||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location, 0);
|
||||
assert!(!event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
id: "focus_in_div",
|
||||
onfocusin: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
id: "focus_out_div",
|
||||
onfocusout: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::DesktopContext;
|
||||
|
||||
pub(crate) fn check_app_exits(app: Component) {
|
||||
use dioxus_desktop::tao::window::WindowBuilder;
|
||||
use dioxus_desktop::Config;
|
||||
// This is a deadman's switch to ensure that the app exits
|
||||
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||
let should_panic_clone = should_panic.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_secs(100));
|
||||
if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
std::process::exit(exitcode::SOFTWARE);
|
||||
}
|
||||
});
|
||||
|
||||
dioxus_desktop::launch_cfg(
|
||||
app,
|
||||
Config::new().with_window(WindowBuilder::new().with_visible(false)),
|
||||
);
|
||||
|
||||
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
check_app_exits(check_html_renders);
|
||||
}
|
||||
|
||||
fn use_inner_html(cx: &ScopeState, id: &'static str) -> Option<String> {
|
||||
let value: &UseRef<Option<String>> = use_ref(cx, || None);
|
||||
use_effect(cx, (), |_| {
|
||||
to_owned![value];
|
||||
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
||||
async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
let html = desktop_context
|
||||
.eval(&format!(
|
||||
r#"let element = document.getElementById('{}');
|
||||
return element.innerHTML;"#,
|
||||
id
|
||||
))
|
||||
.await;
|
||||
if let Ok(serde_json::Value::String(html)) = html {
|
||||
println!("html: {}", html);
|
||||
value.set(Some(html));
|
||||
}
|
||||
}
|
||||
});
|
||||
value.read().clone()
|
||||
}
|
||||
|
||||
const EXPECTED_HTML: &str = r#"<div id="5" style="width: 100px; height: 100px; color: rgb(0, 0, 0);"><input type="checkbox"><h1>text</h1><div><p>hello world</p></div></div>"#;
|
||||
|
||||
fn check_html_renders(cx: Scope) -> Element {
|
||||
let inner_html = use_inner_html(cx, "main_div");
|
||||
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
||||
|
||||
if let Some(raw_html) = inner_html.as_deref() {
|
||||
let fragment = scraper::Html::parse_fragment(raw_html);
|
||||
println!("fragment: {:?}", fragment.html());
|
||||
let expected = scraper::Html::parse_fragment(EXPECTED_HTML);
|
||||
println!("fragment: {:?}", expected.html());
|
||||
if fragment == expected {
|
||||
println!("html matches");
|
||||
desktop_context.close();
|
||||
}
|
||||
}
|
||||
|
||||
let dyn_value = 0;
|
||||
let dyn_element = rsx! {
|
||||
div {
|
||||
dangerous_inner_html: "<p>hello world</p>",
|
||||
}
|
||||
};
|
||||
|
||||
render! {
|
||||
div {
|
||||
id: "main_div",
|
||||
div {
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
color: "rgb({dyn_value}, {dyn_value}, {dyn_value})",
|
||||
id: 5,
|
||||
input {
|
||||
"type": "checkbox",
|
||||
},
|
||||
h1 {
|
||||
"text"
|
||||
}
|
||||
dyn_element
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,17 @@ use wry::{
|
||||
|
||||
// pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
|
||||
|
||||
/// The behaviour of the application when the last window is closed.
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum WindowCloseBehaviour {
|
||||
/// Default behaviour, closing the last window exits the app
|
||||
LastWindowExitsApp,
|
||||
/// Closing the last window will not actually close it, just hide it
|
||||
LastWindowHides,
|
||||
/// Closing the last window will close it but the app will keep running so that new windows can be opened
|
||||
CloseWindow,
|
||||
}
|
||||
|
||||
/// The configuration for the desktop application.
|
||||
pub struct Config {
|
||||
pub(crate) window: WindowBuilder,
|
||||
@@ -23,6 +34,8 @@ pub struct Config {
|
||||
pub(crate) custom_head: Option<String>,
|
||||
pub(crate) custom_index: Option<String>,
|
||||
pub(crate) root_name: String,
|
||||
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
|
||||
pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
|
||||
}
|
||||
|
||||
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
||||
@@ -50,6 +63,8 @@ impl Config {
|
||||
custom_head: None,
|
||||
custom_index: None,
|
||||
root_name: "main".to_string(),
|
||||
background_color: None,
|
||||
last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +102,12 @@ impl Config {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the behaviour of the application when the last window is closed.
|
||||
pub fn with_close_behaviour(mut self, behaviour: WindowCloseBehaviour) -> Self {
|
||||
self.last_window_close_behaviour = behaviour;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a file drop handler. If this is enabled, html drag events will be disabled.
|
||||
pub fn with_file_drop_handler(
|
||||
mut self,
|
||||
@@ -137,6 +158,14 @@ impl Config {
|
||||
self.root_name = name.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background color of the WebView.
|
||||
/// This will be set before the HTML is rendered and can be used to prevent flashing when the page loads.
|
||||
/// Accepts a color in RGBA format
|
||||
pub fn with_background_color(mut self, color: (u8, u8, u8, u8)) -> Self {
|
||||
self.background_color = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
||||
@@ -5,8 +5,7 @@ use std::rc::Weak;
|
||||
use crate::create_new_window;
|
||||
use crate::eval::EvalResult;
|
||||
use crate::events::IpcMessage;
|
||||
use crate::shortcut::IntoKeyCode;
|
||||
use crate::shortcut::IntoModifersState;
|
||||
use crate::query::QueryEngine;
|
||||
use crate::shortcut::ShortcutId;
|
||||
use crate::shortcut::ShortcutRegistry;
|
||||
use crate::shortcut::ShortcutRegistryError;
|
||||
@@ -16,8 +15,8 @@ use dioxus_core::ScopeState;
|
||||
use dioxus_core::VirtualDom;
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use serde_json::Value;
|
||||
use slab::Slab;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
use wry::application::event::Event;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
use wry::application::event_loop::EventLoopWindowTarget;
|
||||
@@ -51,16 +50,15 @@ pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
|
||||
/// ```rust, ignore
|
||||
/// let desktop = cx.consume_context::<DesktopContext>().unwrap();
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct DesktopContext {
|
||||
pub struct DesktopService {
|
||||
/// The wry/tao proxy to the current window
|
||||
pub webview: Rc<WebView>,
|
||||
|
||||
/// The proxy to the event loop
|
||||
pub proxy: ProxyType,
|
||||
|
||||
/// The receiver for eval results since eval is async
|
||||
pub(super) eval: tokio::sync::broadcast::Sender<Value>,
|
||||
/// The receiver for queries about the current window
|
||||
pub(super) query: QueryEngine,
|
||||
|
||||
pub(super) pending_windows: WebviewQueue,
|
||||
|
||||
@@ -74,8 +72,11 @@ pub struct DesktopContext {
|
||||
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
|
||||
}
|
||||
|
||||
/// A handle to the [`DesktopService`] that can be passed around.
|
||||
pub type DesktopContext = Rc<DesktopService>;
|
||||
|
||||
/// A smart pointer to the current window.
|
||||
impl std::ops::Deref for DesktopContext {
|
||||
impl std::ops::Deref for DesktopService {
|
||||
type Target = Window;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@@ -83,9 +84,9 @@ impl std::ops::Deref for DesktopContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopContext {
|
||||
impl DesktopService {
|
||||
pub(crate) fn new(
|
||||
webview: Rc<WebView>,
|
||||
webview: WebView,
|
||||
proxy: ProxyType,
|
||||
event_loop: EventLoopWindowTarget<UserWindowEvent>,
|
||||
webviews: WebviewQueue,
|
||||
@@ -93,10 +94,10 @@ impl DesktopContext {
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> Self {
|
||||
Self {
|
||||
webview,
|
||||
webview: Rc::new(webview),
|
||||
proxy,
|
||||
event_loop,
|
||||
eval: tokio::sync::broadcast::channel(8).0,
|
||||
query: Default::default(),
|
||||
pending_windows: webviews,
|
||||
event_handlers,
|
||||
shortcut_manager,
|
||||
@@ -112,7 +113,7 @@ impl DesktopContext {
|
||||
/// You can use this to control other windows from the current window.
|
||||
///
|
||||
/// Be careful to not create a cycle of windows, or you might leak memory.
|
||||
pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak<WebView> {
|
||||
pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak<DesktopService> {
|
||||
let window = create_new_window(
|
||||
cfg,
|
||||
&self.event_loop,
|
||||
@@ -123,7 +124,13 @@ impl DesktopContext {
|
||||
self.shortcut_manager.clone(),
|
||||
);
|
||||
|
||||
let id = window.webview.window().id();
|
||||
let desktop_context = window
|
||||
.dom
|
||||
.base_scope()
|
||||
.consume_context::<Rc<DesktopService>>()
|
||||
.unwrap();
|
||||
|
||||
let id = window.desktop_context.webview.window().id();
|
||||
|
||||
self.proxy
|
||||
.send_event(UserWindowEvent(EventData::NewWindow, id))
|
||||
@@ -133,11 +140,9 @@ impl DesktopContext {
|
||||
.send_event(UserWindowEvent(EventData::Poll, id))
|
||||
.unwrap();
|
||||
|
||||
let webview = window.webview.clone();
|
||||
|
||||
self.pending_windows.borrow_mut().push(window);
|
||||
|
||||
Rc::downgrade(&webview)
|
||||
Rc::downgrade(&desktop_context)
|
||||
}
|
||||
|
||||
/// trigger the drag-window event
|
||||
@@ -210,28 +215,10 @@ impl DesktopContext {
|
||||
|
||||
/// Evaluate a javascript expression
|
||||
pub fn eval(&self, code: &str) -> EvalResult {
|
||||
// Embed the return of the eval in a function so we can send it back to the main thread
|
||||
let script = format!(
|
||||
r#"
|
||||
window.ipc.postMessage(
|
||||
JSON.stringify({{
|
||||
"method":"eval_result",
|
||||
"params": (
|
||||
function(){{
|
||||
{code}
|
||||
}}
|
||||
)()
|
||||
}})
|
||||
);
|
||||
"#
|
||||
);
|
||||
// the query id lets us keep track of the eval result and send it back to the main thread
|
||||
let query = self.query.new_query(code, &self.webview);
|
||||
|
||||
if let Err(e) = self.webview.evaluate_script(&script) {
|
||||
// send an error to the eval receiver
|
||||
log::warn!("Eval script error: {e}");
|
||||
}
|
||||
|
||||
EvalResult::new(self.eval.clone())
|
||||
EvalResult::new(query)
|
||||
}
|
||||
|
||||
/// Create a wry event handler that listens for wry events.
|
||||
@@ -255,15 +242,11 @@ impl DesktopContext {
|
||||
/// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information.
|
||||
pub fn create_shortcut(
|
||||
&self,
|
||||
key: impl IntoKeyCode,
|
||||
modifiers: impl IntoModifersState,
|
||||
accelerator: Accelerator,
|
||||
callback: impl FnMut() + 'static,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
self.shortcut_manager.add_shortcut(
|
||||
modifiers.into_modifiers_state(),
|
||||
key.into_key_code(),
|
||||
Box::new(callback),
|
||||
)
|
||||
self.shortcut_manager
|
||||
.add_shortcut(accelerator, Box::new(callback))
|
||||
}
|
||||
|
||||
/// Remove a global shortcut
|
||||
@@ -423,7 +406,7 @@ pub fn use_wry_event_handler(
|
||||
let id = desktop.create_wry_event_handler(handler);
|
||||
|
||||
WryEventHandler {
|
||||
handlers: desktop.event_handlers,
|
||||
handlers: desktop.event_handlers.clone(),
|
||||
id,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use dioxus_core::ElementId;
|
||||
use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
|
||||
|
||||
use crate::{desktop_context::DesktopContext, query::QueryEngine};
|
||||
|
||||
/// A mounted element passed to onmounted events
|
||||
pub struct DesktopElement {
|
||||
id: ElementId,
|
||||
webview: DesktopContext,
|
||||
query: QueryEngine,
|
||||
}
|
||||
|
||||
impl DesktopElement {
|
||||
pub(crate) fn new(id: ElementId, webview: DesktopContext, query: QueryEngine) -> Self {
|
||||
Self { id, webview, query }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderedElementBacking for DesktopElement {
|
||||
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn get_client_rect(
|
||||
&self,
|
||||
) -> std::pin::Pin<
|
||||
Box<
|
||||
dyn futures_util::Future<
|
||||
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
|
||||
>,
|
||||
>,
|
||||
> {
|
||||
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
.new_query::<Option<Rect<f64, f64>>>(&script, &self.webview.webview)
|
||||
.resolve();
|
||||
Box::pin(async move {
|
||||
match fut.await {
|
||||
Ok(Some(rect)) => Ok(rect),
|
||||
Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
|
||||
Box::new(DesktopQueryError::FailedToQuery),
|
||||
)),
|
||||
Err(err) => {
|
||||
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn scroll_to(
|
||||
&self,
|
||||
behavior: dioxus_html::ScrollBehavior,
|
||||
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
|
||||
let script = format!(
|
||||
"return window.interpreter.ScrollTo({}, {});",
|
||||
self.id.0,
|
||||
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
|
||||
);
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
.new_query::<bool>(&script, &self.webview.webview)
|
||||
.resolve();
|
||||
Box::pin(async move {
|
||||
match fut.await {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
|
||||
Box::new(DesktopQueryError::FailedToQuery),
|
||||
)),
|
||||
Err(err) => {
|
||||
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_focus(
|
||||
&self,
|
||||
focus: bool,
|
||||
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
|
||||
let script = format!(
|
||||
"return window.interpreter.SetFocus({}, {});",
|
||||
self.id.0, focus
|
||||
);
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
.new_query::<bool>(&script, &self.webview.webview)
|
||||
.resolve();
|
||||
|
||||
Box::pin(async move {
|
||||
match fut.await {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
|
||||
Box::new(DesktopQueryError::FailedToQuery),
|
||||
)),
|
||||
Err(err) => {
|
||||
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DesktopQueryError {
|
||||
FailedToQuery,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DesktopQueryError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DesktopQueryError {}
|
||||
@@ -1,36 +1,32 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::query::Query;
|
||||
use crate::query::QueryError;
|
||||
use crate::use_window;
|
||||
use dioxus_core::ScopeState;
|
||||
use serde::de::Error;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A future that resolves to the result of a JavaScript evaluation.
|
||||
pub struct EvalResult {
|
||||
pub(crate) broadcast: tokio::sync::broadcast::Sender<serde_json::Value>,
|
||||
pub(crate) query: Query<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl EvalResult {
|
||||
pub(crate) fn new(sender: tokio::sync::broadcast::Sender<serde_json::Value>) -> Self {
|
||||
Self { broadcast: sender }
|
||||
pub(crate) fn new(query: Query<serde_json::Value>) -> Self {
|
||||
Self { query }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFuture for EvalResult {
|
||||
type Output = Result<serde_json::Value, serde_json::Error>;
|
||||
type Output = Result<serde_json::Value, QueryError>;
|
||||
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut reciever = self.broadcast.subscribe();
|
||||
match reciever.recv().await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err(serde_json::Error::custom("No result returned")),
|
||||
}
|
||||
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
|
||||
Box::pin(self.query.resolve())
|
||||
as Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
#![allow(unused)]
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct FileDiologRequest {
|
||||
accept: String,
|
||||
pub(crate) struct FileDialogRequest {
|
||||
#[serde(default)]
|
||||
accept: Option<String>,
|
||||
multiple: bool,
|
||||
pub event: String,
|
||||
pub target: usize,
|
||||
pub bubbles: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn get_file_event(request: &FileDiologRequest) -> Vec<PathBuf> {
|
||||
#[cfg(not(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
)))]
|
||||
pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec<PathBuf> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec<PathBuf> {
|
||||
let mut dialog = rfd::FileDialog::new();
|
||||
|
||||
let filters: Vec<_> = request
|
||||
.accept
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.filter_map(|s| Filters::from_str(s).ok())
|
||||
.collect();
|
||||
|
||||
+161
-61
@@ -5,26 +5,36 @@
|
||||
|
||||
mod cfg;
|
||||
mod desktop_context;
|
||||
mod element;
|
||||
mod escape;
|
||||
mod eval;
|
||||
mod events;
|
||||
mod file_upload;
|
||||
mod protocol;
|
||||
mod query;
|
||||
mod shortcut;
|
||||
mod waker;
|
||||
mod webview;
|
||||
|
||||
pub use cfg::Config;
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
mod mobile_shortcut;
|
||||
|
||||
use crate::query::QueryResult;
|
||||
pub use cfg::{Config, WindowCloseBehaviour};
|
||||
pub use desktop_context::DesktopContext;
|
||||
pub use desktop_context::{
|
||||
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
|
||||
use_window, use_wry_event_handler, DesktopService, WryEventHandler, WryEventHandlerId,
|
||||
};
|
||||
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
||||
use dioxus_core::*;
|
||||
use dioxus_html::MountedData;
|
||||
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
||||
use element::DesktopElement;
|
||||
pub use eval::{use_eval, EvalResult};
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use shortcut::ShortcutRegistry;
|
||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use std::task::Waker;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
@@ -112,17 +122,19 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
|
||||
let window_behaviour = cfg.last_window_close_behaviour;
|
||||
|
||||
// Intialize hot reloading if it is enabled
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
{
|
||||
dioxus_hot_reload::connect({
|
||||
let proxy = proxy.clone();
|
||||
dioxus_hot_reload::connect(move |template| {
|
||||
move |template| {
|
||||
let _ = proxy.send_event(UserWindowEvent(
|
||||
EventData::HotReloadEvent(template),
|
||||
unsafe { WindowId::dummy() },
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// We start the tokio runtime *on this thread*
|
||||
// Any future we poll later will use this runtime to spawn tasks and for IO
|
||||
@@ -145,16 +157,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
|
||||
let shortcut_manager = ShortcutRegistry::new(&event_loop);
|
||||
|
||||
// By default, we'll create a new window when the app starts
|
||||
queue.borrow_mut().push(create_new_window(
|
||||
cfg,
|
||||
&event_loop,
|
||||
&proxy,
|
||||
VirtualDom::new_with_props(root, props),
|
||||
&queue,
|
||||
&event_handlers,
|
||||
shortcut_manager.clone(),
|
||||
));
|
||||
// move the props into a cell so we can pop it out later to create the first window
|
||||
// iOS panics if we create a window before the event loop is started
|
||||
let props = Rc::new(Cell::new(Some(props)));
|
||||
let cfg = Rc::new(Cell::new(Some(cfg)));
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
@@ -165,27 +171,61 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
webviews.remove(&window_id);
|
||||
WindowEvent::CloseRequested => match window_behaviour {
|
||||
cfg::WindowCloseBehaviour::LastWindowExitsApp => {
|
||||
webviews.remove(&window_id);
|
||||
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
}
|
||||
cfg::WindowCloseBehaviour::LastWindowHides => {
|
||||
let Some(webview) = webviews.get(&window_id) else {
|
||||
return;
|
||||
};
|
||||
hide_app_window(&webview.desktop_context.webview);
|
||||
}
|
||||
cfg::WindowCloseBehaviour::CloseWindow => {
|
||||
webviews.remove(&window_id);
|
||||
}
|
||||
},
|
||||
WindowEvent::Destroyed { .. } => {
|
||||
webviews.remove(&window_id);
|
||||
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
if matches!(
|
||||
window_behaviour,
|
||||
cfg::WindowCloseBehaviour::LastWindowExitsApp
|
||||
) && webviews.is_empty()
|
||||
{
|
||||
*control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::NewEvents(StartCause::Init)
|
||||
| Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => {
|
||||
Event::NewEvents(StartCause::Init) => {
|
||||
//
|
||||
let props = props.take().unwrap();
|
||||
let cfg = cfg.take().unwrap();
|
||||
|
||||
let handler = create_new_window(
|
||||
cfg,
|
||||
event_loop,
|
||||
&proxy,
|
||||
VirtualDom::new_with_props(root, props),
|
||||
&queue,
|
||||
&event_handlers,
|
||||
shortcut_manager.clone(),
|
||||
);
|
||||
|
||||
let id = handler.desktop_context.webview.window().id();
|
||||
webviews.insert(id, handler);
|
||||
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
|
||||
}
|
||||
|
||||
Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => {
|
||||
for handler in queue.borrow_mut().drain(..) {
|
||||
let id = handler.webview.window().id();
|
||||
let id = handler.desktop_context.webview.window().id();
|
||||
webviews.insert(id, handler);
|
||||
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
|
||||
}
|
||||
@@ -221,37 +261,66 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "user_event" => {
|
||||
let evt = match serde_json::from_value::<HtmlEvent>(msg.params()) {
|
||||
let params = msg.params();
|
||||
|
||||
let evt = match serde_json::from_value::<HtmlEvent>(params) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let HtmlEvent {
|
||||
element,
|
||||
name,
|
||||
bubbles,
|
||||
data,
|
||||
} = evt;
|
||||
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
|
||||
view.dom
|
||||
.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles);
|
||||
// check for a mounted event placeholder and replace it with a desktop specific element
|
||||
let as_any = if let dioxus_html::EventData::Mounted = &data {
|
||||
let query = view
|
||||
.dom
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.query
|
||||
.clone();
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.webview);
|
||||
let element =
|
||||
DesktopElement::new(element, view.desktop_context.clone(), query);
|
||||
|
||||
Rc::new(MountedData::new(element))
|
||||
} else {
|
||||
data.into_any()
|
||||
};
|
||||
|
||||
view.dom.handle_event(&name, as_any, element, bubbles);
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
}
|
||||
|
||||
// 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
|
||||
EventData::Ipc(msg) if msg.method() == "query" => {
|
||||
let params = msg.params();
|
||||
|
||||
if let Ok(result) = serde_json::from_value::<QueryResult>(params) {
|
||||
let view = webviews.get(&event.1).unwrap();
|
||||
let query = view
|
||||
.dom
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.query
|
||||
.clone();
|
||||
|
||||
query.send(result);
|
||||
}
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
send_edits(view.dom.rebuild(), &view.webview);
|
||||
}
|
||||
|
||||
// When the webview chirps back with the result of the eval, we send it to the active receiver
|
||||
//
|
||||
// This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once,
|
||||
// you might the wrong result. This should be fixed
|
||||
EventData::Ipc(msg) if msg.method() == "eval_result" => {
|
||||
webviews[&event.1]
|
||||
.dom
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.eval
|
||||
.send(msg.params())
|
||||
.unwrap();
|
||||
send_edits(view.dom.rebuild(), &view.desktop_context.webview);
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "browser_open" => {
|
||||
@@ -267,7 +336,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "file_diolog" => {
|
||||
if let Ok(file_diolog) =
|
||||
serde_json::from_value::<file_upload::FileDiologRequest>(msg.params())
|
||||
serde_json::from_value::<file_upload::FileDialogRequest>(msg.params())
|
||||
{
|
||||
let id = ElementId(file_diolog.target);
|
||||
let event_name = &file_diolog.event;
|
||||
@@ -289,7 +358,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.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,9 +380,8 @@ fn create_new_window(
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> WebviewHandler {
|
||||
let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
|
||||
dom.base_scope().provide_context(DesktopContext::new(
|
||||
webview.clone(),
|
||||
let desktop_context = Rc::from(DesktopService::new(
|
||||
webview,
|
||||
proxy.clone(),
|
||||
event_loop.clone(),
|
||||
queue.clone(),
|
||||
@@ -321,24 +389,25 @@ fn create_new_window(
|
||||
shortcut_manager,
|
||||
));
|
||||
|
||||
let id = webview.window().id();
|
||||
dom.base_scope().provide_context(desktop_context.clone());
|
||||
|
||||
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
|
||||
WebviewHandler {
|
||||
webview,
|
||||
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
|
||||
waker: waker::tao_waker(proxy, desktop_context.webview.window().id()),
|
||||
desktop_context,
|
||||
dom,
|
||||
waker: waker::tao_waker(proxy, id),
|
||||
web_context,
|
||||
_web_context: web_context,
|
||||
}
|
||||
}
|
||||
|
||||
struct WebviewHandler {
|
||||
dom: VirtualDom,
|
||||
webview: Rc<wry::webview::WebView>,
|
||||
desktop_context: DesktopContext,
|
||||
waker: Waker,
|
||||
// This is nessisary because of a bug in wry. Wry assumes the webcontext is alive for the lifetime of the webview. We need to keep the webcontext alive, otherwise the webview will crash
|
||||
#[allow(dead_code)]
|
||||
web_context: WebContext,
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
/// Poll the virtualdom until it's pending
|
||||
@@ -360,7 +429,7 @@ fn poll_vdom(view: &mut WebviewHandler) {
|
||||
}
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,3 +440,34 @@ fn send_edits(edits: Mutations, webview: &WebView) {
|
||||
// todo: use SSE and binary data to send the edits with lower overhead
|
||||
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
|
||||
}
|
||||
|
||||
/// Different hide implementations per platform
|
||||
#[allow(unused)]
|
||||
fn hide_app_window(webview: &WebView) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use wry::application::platform::windows::WindowExtWindows;
|
||||
webview.window().set_visible(false);
|
||||
webview.window().set_skip_taskbar(true);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use wry::application::platform::unix::WindowExtUnix;
|
||||
webview.window().set_visible(false);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// webview.window().set_visible(false); has the wrong behaviour on macOS
|
||||
// It will hide the window but not show it again when the user switches
|
||||
// back to the app. `NSApplication::hide:` has the correct behaviour
|
||||
use objc::runtime::Object;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
objc::rc::autoreleasepool(|| unsafe {
|
||||
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
|
||||
let nil = std::ptr::null_mut::<Object>();
|
||||
let _: () = msg_send![app, hide: nil];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use super::*;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
use wry::application::event_loop::EventLoopWindowTarget;
|
||||
|
||||
pub struct GlobalShortcut();
|
||||
pub struct ShortcutManager();
|
||||
|
||||
impl ShortcutManager {
|
||||
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
|
||||
Self()
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&mut self,
|
||||
accelerator: Accelerator,
|
||||
) -> Result<GlobalShortcut, ShortcutManagerError> {
|
||||
Ok(GlobalShortcut())
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self, id: ShortcutId) -> Result<(), ShortcutManagerError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unregister_all(&mut self) -> Result<(), ShortcutManagerError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
use std::{error, fmt};
|
||||
|
||||
/// An error whose cause the `ShortcutManager` to fail.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum ShortcutManagerError {
|
||||
AcceleratorAlreadyRegistered(Accelerator),
|
||||
AcceleratorNotRegistered(Accelerator),
|
||||
InvalidAccelerator(String),
|
||||
}
|
||||
|
||||
impl error::Error for ShortcutManagerError {}
|
||||
impl fmt::Display for ShortcutManagerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
ShortcutManagerError::AcceleratorAlreadyRegistered(e) => {
|
||||
f.pad(&format!("hotkey already registered: {:?}", e))
|
||||
}
|
||||
ShortcutManagerError::AcceleratorNotRegistered(e) => {
|
||||
f.pad(&format!("hotkey not registered: {:?}", e))
|
||||
}
|
||||
ShortcutManagerError::InvalidAccelerator(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use dioxus_interpreter_js::INTERPRETER_JS;
|
||||
use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
@@ -15,12 +15,12 @@ fn module_loader(root_name: &str) -> String {
|
||||
let inputs = document.querySelectorAll("input");
|
||||
for (let input of inputs) {
|
||||
if (!input.getAttribute("data-dioxus-file-listener")) {
|
||||
input.setAttribute("data-dioxus-file-listener", true);
|
||||
input.addEventListener("click", (event) => {
|
||||
let target = event.target;
|
||||
// prevent file inputs from opening the file dialog on click
|
||||
const type = target.getAttribute("type");
|
||||
if (type === "file") {
|
||||
// prevent file inputs from opening the file dialog on click
|
||||
const type = input.getAttribute("type");
|
||||
if (type === "file") {
|
||||
input.setAttribute("data-dioxus-file-listener", true);
|
||||
input.addEventListener("click", (event) => {
|
||||
let target = event.target;
|
||||
let target_id = find_real_id(target);
|
||||
if (target_id !== null) {
|
||||
const send = (event_name) => {
|
||||
@@ -30,20 +30,20 @@ fn module_loader(root_name: &str) -> String {
|
||||
send("change&input");
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
format!(
|
||||
r#"
|
||||
<script>
|
||||
<script type="module">
|
||||
{js}
|
||||
|
||||
let rootname = "{root_name}";
|
||||
let root = window.document.getElementById(rootname);
|
||||
if (root != null) {{
|
||||
window.interpreter = new Interpreter(root);
|
||||
window.interpreter = new Interpreter(root, new InterpreterConfig(true));
|
||||
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
||||
}}
|
||||
</script>
|
||||
@@ -84,10 +84,17 @@ pub(super) fn desktop_handler(
|
||||
.header("Content-Type", "text/html")
|
||||
.body(Cow::from(body))
|
||||
.map_err(From::from);
|
||||
} else if request.uri().path() == "/common.js" {
|
||||
return Response::builder()
|
||||
.header("Content-Type", "text/javascript")
|
||||
.body(Cow::from(COMMON_JS.as_bytes()))
|
||||
.map_err(From::from);
|
||||
}
|
||||
|
||||
// Else, try to serve a file from the filesystem.
|
||||
let path = PathBuf::from(request.uri().path().trim_start_matches('/'));
|
||||
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
|
||||
.expect("expected URL to be UTF-8 encoded");
|
||||
let path = PathBuf::from(&*decoded);
|
||||
|
||||
// If the path is relative, we'll try to serve it from the assets directory.
|
||||
let mut asset = get_asset_root()
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde_json::Value;
|
||||
use slab::Slab;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use wry::webview::WebView;
|
||||
|
||||
/// Tracks what query ids are currently active
|
||||
#[derive(Default, Clone)]
|
||||
struct SharedSlab {
|
||||
slab: Rc<RefCell<Slab<()>>>,
|
||||
}
|
||||
|
||||
/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct QueryEngine {
|
||||
sender: Rc<tokio::sync::broadcast::Sender<QueryResult>>,
|
||||
active_requests: SharedSlab,
|
||||
}
|
||||
|
||||
impl Default for QueryEngine {
|
||||
fn default() -> Self {
|
||||
let (sender, _) = tokio::sync::broadcast::channel(1000);
|
||||
Self {
|
||||
sender: Rc::new(sender),
|
||||
active_requests: SharedSlab::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryEngine {
|
||||
/// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id.
|
||||
pub fn new_query<V: DeserializeOwned>(&self, script: &str, webview: &WebView) -> Query<V> {
|
||||
let request_id = self.active_requests.slab.borrow_mut().insert(());
|
||||
|
||||
// start the query
|
||||
// We embed the return of the eval in a function so we can send it back to the main thread
|
||||
if let Err(err) = webview.evaluate_script(&format!(
|
||||
r#"window.ipc.postMessage(
|
||||
JSON.stringify({{
|
||||
"method":"query",
|
||||
"params": {{
|
||||
"id": {request_id},
|
||||
"data": (function(){{{script}}})()
|
||||
}}
|
||||
}})
|
||||
);"#
|
||||
)) {
|
||||
log::warn!("Query error: {err}");
|
||||
}
|
||||
|
||||
Query {
|
||||
slab: self.active_requests.clone(),
|
||||
id: request_id,
|
||||
reciever: self.sender.subscribe(),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a query result
|
||||
pub fn send(&self, data: QueryResult) {
|
||||
let _ = self.sender.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Query<V: DeserializeOwned> {
|
||||
slab: SharedSlab,
|
||||
id: usize,
|
||||
reciever: tokio::sync::broadcast::Receiver<QueryResult>,
|
||||
phantom: std::marker::PhantomData<V>,
|
||||
}
|
||||
|
||||
impl<V: DeserializeOwned> Query<V> {
|
||||
/// Resolve the query
|
||||
pub async fn resolve(mut self) -> Result<V, QueryError> {
|
||||
let result = loop {
|
||||
match self.reciever.recv().await {
|
||||
Ok(result) => {
|
||||
if result.id == self.id {
|
||||
break V::deserialize(result.data).map_err(QueryError::DeserializeError);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
break Err(QueryError::RecvError(err));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Remove the query from the slab
|
||||
self.slab.slab.borrow_mut().remove(self.id);
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum QueryError {
|
||||
#[error("Error receiving query result: {0}")]
|
||||
RecvError(RecvError),
|
||||
#[error("Error deserializing query result: {0}")]
|
||||
DeserializeError(serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub(crate) struct QueryResult {
|
||||
id: usize,
|
||||
data: Value,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
|
||||
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus_html::input_data::keyboard_types::Modifiers;
|
||||
@@ -6,11 +6,24 @@ use slab::Slab;
|
||||
use wry::application::{
|
||||
accelerator::{Accelerator, AcceleratorId},
|
||||
event_loop::EventLoopWindowTarget,
|
||||
global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError},
|
||||
keyboard::{KeyCode, ModifiersState},
|
||||
};
|
||||
|
||||
use crate::{use_window, DesktopContext};
|
||||
use crate::{desktop_context::DesktopContext, use_window};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
use wry::application::global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError};
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
pub use crate::mobile_shortcut::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ShortcutRegistry {
|
||||
@@ -21,6 +34,7 @@ pub(crate) struct ShortcutRegistry {
|
||||
type ShortcutMap = Rc<RefCell<HashMap<AcceleratorId, Shortcut>>>;
|
||||
|
||||
struct Shortcut {
|
||||
#[allow(unused)]
|
||||
shortcut: GlobalShortcut,
|
||||
callbacks: Slab<Box<dyn FnMut()>>,
|
||||
}
|
||||
@@ -41,17 +55,10 @@ impl Shortcut {
|
||||
|
||||
impl ShortcutRegistry {
|
||||
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
|
||||
let myself = Self {
|
||||
Self {
|
||||
manager: Rc::new(RefCell::new(ShortcutManager::new(target))),
|
||||
shortcuts: Rc::new(RefCell::new(HashMap::new())),
|
||||
};
|
||||
// prevent CTRL+R from reloading the page which breaks apps
|
||||
let _ = myself.add_shortcut(
|
||||
Some(ModifiersState::CONTROL),
|
||||
KeyCode::KeyR,
|
||||
Box::new(|| {}),
|
||||
);
|
||||
myself
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call_handlers(&self, id: AcceleratorId) {
|
||||
@@ -64,11 +71,9 @@ impl ShortcutRegistry {
|
||||
|
||||
pub(crate) fn add_shortcut(
|
||||
&self,
|
||||
modifiers: impl Into<Option<ModifiersState>>,
|
||||
key: KeyCode,
|
||||
accelerator: Accelerator,
|
||||
callback: Box<dyn FnMut()>,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
let accelerator = Accelerator::new(modifiers, key);
|
||||
let accelerator_id = accelerator.clone().id();
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
Ok(
|
||||
@@ -107,8 +112,17 @@ impl ShortcutRegistry {
|
||||
if let Some(callbacks) = shortcuts.get_mut(&id.id) {
|
||||
callbacks.remove(id.number);
|
||||
if callbacks.is_empty() {
|
||||
if let Some(shortcut) = shortcuts.remove(&id.id) {
|
||||
let _ = self.manager.borrow_mut().unregister(shortcut.shortcut);
|
||||
if let Some(_shortcut) = shortcuts.remove(&id.id) {
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,12 +132,6 @@ impl ShortcutRegistry {
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
shortcuts.clear();
|
||||
let _ = self.manager.borrow_mut().unregister_all();
|
||||
// prevent CTRL+R from reloading the page which breaks apps
|
||||
let _ = self.add_shortcut(
|
||||
Some(ModifiersState::CONTROL),
|
||||
KeyCode::KeyR,
|
||||
Box::new(|| {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,22 +159,45 @@ pub struct ShortcutHandle {
|
||||
pub shortcut_id: ShortcutId,
|
||||
}
|
||||
|
||||
pub trait IntoAccelerator {
|
||||
fn accelerator(&self) -> Accelerator;
|
||||
}
|
||||
|
||||
impl IntoAccelerator for (dioxus_html::KeyCode, ModifiersState) {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(Some(self.1), self.0.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for (ModifiersState, dioxus_html::KeyCode) {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(Some(self.0), self.1.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for dioxus_html::KeyCode {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(None, self.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for &str {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::from_str(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_global_shortcut(
|
||||
cx: &ScopeState,
|
||||
key: impl IntoKeyCode,
|
||||
modifiers: impl IntoModifersState,
|
||||
accelerator: impl IntoAccelerator,
|
||||
handler: impl FnMut() + 'static,
|
||||
) -> &Result<ShortcutHandle, ShortcutRegistryError> {
|
||||
let desktop = use_window(cx);
|
||||
cx.use_hook(move || {
|
||||
let desktop = desktop.clone();
|
||||
|
||||
let id = desktop.create_shortcut(
|
||||
key.into_key_code(),
|
||||
modifiers.into_modifiers_state(),
|
||||
handler,
|
||||
);
|
||||
let id = desktop.create_shortcut(accelerator.accelerator(), handler);
|
||||
|
||||
Ok(ShortcutHandle {
|
||||
desktop,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::desktop_context::EventData;
|
||||
use crate::protocol;
|
||||
use crate::{desktop_context::UserWindowEvent, Config};
|
||||
@@ -13,7 +11,7 @@ pub fn build(
|
||||
cfg: &mut Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> (Rc<WebView>, WebContext) {
|
||||
) -> (WebView, WebContext) {
|
||||
let builder = cfg.window.clone();
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
@@ -62,6 +60,10 @@ pub fn build(
|
||||
webview = webview.with_browser_accelerator_keys(false);
|
||||
}
|
||||
|
||||
if let Some(color) = cfg.background_color {
|
||||
webview = webview.with_background_color(color);
|
||||
}
|
||||
|
||||
// These are commented out because wry is currently broken in wry
|
||||
// let mut web_context = WebContext::new(cfg.data_dir.clone());
|
||||
// .with_web_context(&mut web_context);
|
||||
@@ -90,5 +92,5 @@ pub fn build(
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
|
||||
(Rc::new(webview.build().unwrap()), web_context)
|
||||
(webview.build().unwrap(), web_context)
|
||||
}
|
||||
Reference in new issue
Block a user