Skip to content

Commit

Permalink
Implement draft RPC API. (#95)
Browse files Browse the repository at this point in the history
* Implement draft RPC API.

Remove old Callback mechanism.

* Remove obsolete Callback API
* Remove FuncCall and RPC
* Update README
* Rename set_rpc_handler() to set_handler()
* Use shared rpc_proxy() function for platform consistency
* Improve handling of promise cleanup
* Update README with RPC API info.
* Panic if webview handler is set after window creation.
* Improve rpc_proxy() logic, try to ensure any corresponding promise is always removed.
* Remove FuncCall wrapper.

* Remove set_handler().

Use second argument to add_window_with_configs() to set an RpcHandler.

* Fix windows type signature.

* Tidy obsolete comments and code.

* Remove promise fallback clean up code.

So that rust rpc handlers can perform asynchronous tasks and defer
promise evaluation until later.

If an rpc handler returns None then the handler takes responsibility for
ensuring the corresponding promise is resolved or rejected. If the
request contains an `id` then the handler *must* ensure it evaluates
either `RpcResponse::into_result_script()` or
`RpcResponse::into_error_script()`.

* Remove Sync bound from RpcHandler.

Update multiwindow example so it is slightly more illustrative of
a real-world use case. Now it launches a window when a button is clicked
in the main webview. Multiple windows can be launched and the URL for
the new window is passed from the Javascript code.

* Remove urlencoding from examples.
  • Loading branch information
tmpfs authored Mar 4, 2021
1 parent 600e551 commit e215157
Show file tree
Hide file tree
Showing 13 changed files with 257 additions and 428 deletions.
3 changes: 0 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ url = "2.2"
image = "0.23"
infer = "0.3"

[dev-dependencies]
urlencoding = "1"

[target.'cfg(target_os = "linux")'.dependencies]
cairo-rs = "0.9"
webkit2gtk = { version = "0.11", features = ["v2_8"] }
Expand Down
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,68 @@ cargo run --example multiwindow

For more information, please read the documentation below.

## Rust <-> Javascript

Communication between the host Rust code and webview Javascript is done via [JSON-RPC][].

Embedding code should pass an `RpcHandler` to `add_window_with_configs()` to register an incoming request handler and reply with responses that are passed back to Javascript. On the Javascript side the client is exposed via `window.rpc` with two public methods

1. The `call()` function accepts a method name and parameters and expects a reply.
2. The `notify()` function accepts a method name and parameters but does not expect a reply.

Both functions return promises but `notify()` resolves immediately.

For example in Rust:

```rust
use wry::{Application, Result, WindowProxy, RpcRequest, RpcResponse};

fn main() -> Result<()> {
let mut app = Application::new()?;
let handler = Box::new(|proxy: &WindowProxy, mut req: RpcRequest| {
// Handle the request of type `RpcRequest` and reply with `Option<RpcResponse>`,
// use the `req.method` field to determine which action to take.
//
// If the handler returns a `RpcResponse` it is immediately evaluated
// in the calling webview.
//
// Use the `WindowProxy` to modify the window, eg: `set_fullscreen` etc.
//
// If the request is a notification (no `id` field) then the handler
// can just return `None`.
//
// If an `id` field is present and the handler wants to execute asynchronous
// code it can return `None` but then *must* later evaluate either
// `RpcResponse::into_result_script()` or `RpcResponse::into_error_script()`
// in the webview to ensure the promise is resolved or rejected and removed
// from the cache.
None
});
app.add_window_with_configs(Default::default(), Some(handler), None)?;
app.run();
Ok(())
}
```

Then in Javascript use `call()` to call a remote method and get a response:

```javascript
async function callRemoteMethod() {
let result = await window.rpc.call('remoteMethod', param1, param2);
// Do something with the result
}
```

If Javascript code wants to use a callback style it is easy to alias a function to a method call:

```javascript
function someRemoteMethod() {
return window.rpc.call(arguments.callee.name, Array.prototype.slice(arguments, 0));
}
```

See the `rpc` example for more details.

## [Documentation](https://docs.rs/wry)

## Platform-specific notes
Expand Down Expand Up @@ -84,3 +146,5 @@ WebView2 provided by Microsoft Edge Chromium is used. So wry supports Windows 7,

## License
Apache-2.0/MIT

[JSON-RPC]: https://www.jsonrpc.org
98 changes: 54 additions & 44 deletions examples/multiwindow.rs
Original file line number Diff line number Diff line change
@@ -1,63 +1,73 @@
use wry::Result;
use wry::{Application, Attributes, Callback};
use wry::{Application, Attributes, WindowProxy, RpcRequest};
use serde_json::Value;

fn main() -> Result<()> {
let mut app = Application::new()?;

let html = r#"
<script>
async function openWindow() {
await window.rpc.notify("openWindow", "https://i.imgur.com/x6tXcr9.gif");
}
</script>
<p>Multiwindow example</p>
<button onclick="openWindow();">Launch window</button>
"#;

let attributes = Attributes {
url: Some("https://tauri.studio".to_string()),
url: Some(format!("data:text/html,{}", html)),
// Initialization scripts can be used to define javascript functions and variables.
initialization_scripts: vec![
String::from("breads = NaN"),
String::from("menacing = 'ゴ'"),
/* Custom initialization scripts go here */
],
..Default::default()
};
// Callback defines a rust function to be called on javascript side later. Below is a function
// which will print the list of parameters after 8th calls.
let callback = Callback {
name: "world".to_owned(),
function: Box::new(|proxy, sequence, requests| {
// Proxy is like a window handle for you to send message events to the corresponding webview
// window. You can use it to adjust window and evaluate javascript code like below.
// This is useful when you want to perform any action in javascript.
proxy.evaluate_script("console.log(menacing);")?;
// Sequence is a number counting how many times this function being called.
if sequence < 8 {
println!("{} seconds has passed.", sequence);
} else {
// Requests is a vector of parameters passed from the caller.
println!("{:?}", requests);
}
Ok(())
}),
};

let window1 = app.add_window_with_configs(attributes, Some(vec![callback]), None)?;
let app_proxy = app.application_proxy();
let (window_tx, window_rx) = std::sync::mpsc::channel::<String>();

std::thread::spawn(move || {
for _ in 0..7 {
std::thread::sleep(std::time::Duration::from_secs(1));
window1.evaluate_script("world()".to_string()).unwrap();
let handler = Box::new(move |_proxy: &WindowProxy, req: RpcRequest| {
if &req.method == "openWindow" {
if let Some(params) = req.params {
if let Value::Array(mut arr) = params {
let mut param = if arr.get(0).is_none() {
None
} else {
Some(arr.swap_remove(0))
};

if let Some(param) = param.take() {
if let Value::String(url) = param {
let _ = window_tx.send(url);
}
}
}
}
}
std::thread::sleep(std::time::Duration::from_secs(1));
None
});

let _ = app.add_window_with_configs(attributes, Some(handler), None)?;

window1.set_title("WRYYYYYYYYYYYYYYYYYYYYY").unwrap();
let window2 = app_proxy
.add_window_with_configs(
Attributes {
width: 426.,
height: 197.,
title: "RODA RORA DA".into(),
url: Some("https://i.imgur.com/x6tXcr9.gif".to_string()),
..Default::default()
},
None,
None,
)
.unwrap();
println!("ID of second window: {:?}", window2.id());
std::thread::spawn(move || {
while let Ok(url) = window_rx.recv() {
let new_window = app_proxy
.add_window_with_configs(
Attributes {
width: 426.,
height: 197.,
title: "RODA RORA DA".into(),
url: Some(url),
..Default::default()
},
None,
None,
)
.unwrap();
println!("ID of new window: {:?}", new_window.id());

}
});

app.run();
Expand Down
12 changes: 5 additions & 7 deletions examples/rpc.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use wry::Result;
use wry::{Application, Attributes, RpcResponse};
use wry::{Application, Attributes, RpcResponse, RpcRequest, WindowProxy};
use serde::{Serialize, Deserialize};
use serde_json::Value;

Expand Down Expand Up @@ -31,14 +31,12 @@ async function getAsyncRpcResult() {
<div id="rpc-result"></div>
"#;

let markup = urlencoding::encode(html);
let attributes = Attributes {
url: Some(format!("data:text/html,{}", markup)),
url: Some(format!("data:text/html,{}", html)),
..Default::default()
};

// NOTE: must be set before calling add_window().
app.set_rpc_handler(Box::new(|proxy, mut req| {
let handler = Box::new(|proxy: &WindowProxy, mut req: RpcRequest| {
let mut response = None;
if &req.method == "fullscreen" {
if let Some(params) = req.params.take() {
Expand Down Expand Up @@ -69,9 +67,9 @@ async function getAsyncRpcResult() {
}

response
}));
});

app.add_window(attributes)?;
app.add_window_with_configs(attributes, Some(handler), None)?;

app.run();
Ok(())
Expand Down
42 changes: 10 additions & 32 deletions src/application/general.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
application::{App, AppProxy, InnerWebViewAttributes, InnerWindowAttributes},
ApplicationProxy, Attributes, Callback, CustomProtocol, Error, Icon, Message, Result, WebView,
ApplicationProxy, Attributes, CustomProtocol, Error, Icon, Message, Result, WebView,
WebViewBuilder, WindowMessage, WindowProxy, RpcHandler,
};
#[cfg(target_os = "macos")]
Expand All @@ -13,7 +13,7 @@ use winit::{
window::{Fullscreen, Icon as WinitIcon, Window, WindowAttributes, WindowBuilder},
};

use std::{sync::Arc, collections::HashMap, sync::mpsc::channel};
use std::{collections::HashMap, sync::mpsc::channel};

#[cfg(target_os = "windows")]
use {
Expand Down Expand Up @@ -48,14 +48,14 @@ impl AppProxy for InnerApplicationProxy {
fn add_window(
&self,
attributes: Attributes,
callbacks: Option<Vec<Callback>>,
rpc_handler: Option<RpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<WindowId> {
let (sender, receiver) = channel();
self.send_message(Message::NewWindow(
attributes,
callbacks,
sender,
rpc_handler,
custom_protocol,
))?;
Ok(receiver.recv()?)
Expand Down Expand Up @@ -105,7 +105,6 @@ pub struct InnerApplication {
webviews: HashMap<WindowId, WebView>,
event_loop: EventLoop<Message>,
event_loop_proxy: EventLoopProxy,
pub(crate) rpc_handler: Option<Arc<RpcHandler>>,
}

impl App for InnerApplication {
Expand All @@ -119,14 +118,13 @@ impl App for InnerApplication {
webviews: HashMap::new(),
event_loop,
event_loop_proxy: proxy,
rpc_handler: None,
})
}

fn create_webview(
&mut self,
attributes: Attributes,
callbacks: Option<Vec<Callback>>,
rpc_handler: Option<RpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<Self::Id> {
let (window_attrs, webview_attrs) = attributes.split();
Expand All @@ -135,9 +133,8 @@ impl App for InnerApplication {
&self.application_proxy(),
window,
webview_attrs,
callbacks,
custom_protocol,
self.rpc_handler.clone(),
rpc_handler,
)?;
let id = webview.window().id();
self.webviews.insert(id, webview);
Expand All @@ -153,7 +150,6 @@ impl App for InnerApplication {
fn run(self) {
let dispatcher = self.application_proxy();
let mut windows = self.webviews;
let rpc_handler = self.rpc_handler.clone();
self.event_loop.run(move |event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;

Expand All @@ -175,17 +171,16 @@ impl App for InnerApplication {
_ => {}
},
Event::UserEvent(message) => match message {
Message::NewWindow(attributes, callbacks, sender, custom_protocol) => {
Message::NewWindow(attributes, sender, rpc_handler, custom_protocol) => {
let (window_attrs, webview_attrs) = attributes.split();
let window = _create_window(&event_loop, window_attrs).unwrap();
sender.send(window.id()).unwrap();
let webview = _create_webview(
&dispatcher,
window,
webview_attrs,
callbacks,
custom_protocol,
rpc_handler.clone(),
rpc_handler,
)
.unwrap();
let id = webview.window().id();
Expand Down Expand Up @@ -347,9 +342,8 @@ fn _create_webview(
dispatcher: &InnerApplicationProxy,
window: Window,
attributes: InnerWebViewAttributes,
callbacks: Option<Vec<Callback>>,
custom_protocol: Option<CustomProtocol>,
rpc_handler: Option<Arc<RpcHandler>>,
rpc_handler: Option<RpcHandler>,
) -> Result<WebView> {
let window_id = window.id();
let rpc_win_id = window_id.clone();
Expand All @@ -359,23 +353,7 @@ fn _create_webview(
for js in attributes.initialization_scripts {
webview = webview.initialize_script(&js);
}
if let Some(cbs) = callbacks {
for Callback { name, mut function } in cbs {
let dispatcher = dispatcher.clone();
webview = webview.add_callback(&name, move |_, seq, req| {
function(
WindowProxy::new(
ApplicationProxy {
inner: dispatcher.clone(),
},
window_id,
),
seq,
req,
)
});
}
}

if let Some(protocol) = custom_protocol {
webview = webview.register_protocol(protocol.name, protocol.handler)
}
Expand Down
Loading

0 comments on commit e215157

Please sign in to comment.