Boscop / web-view

Rust bindings for webview, a tiny cross-platform library to render web-based GUIs for desktop applications
MIT License
1.92k stars 175 forks source link

Two-way bindings between Webview and Rust Yew #135

Open antebandov opened 4 years ago

antebandov commented 4 years ago

How can i call the invoke_handler from a Yew application? I want to communicate between Yew and Webview. I already tried adding javascript to Yew through stdweb and then calling external.invoke("foo"). But this doesn't work.

mbuscemi commented 4 years ago

I have not yet tried stdweb. I went with wasm_bindgen. It compiles in Rust, but errors out in JS at runtime. This happens even when just including wasm_bindgen and not invoking any functions (source code). I'm curious if there's a suggested path forward for this.

mbuscemi commented 4 years ago

external.invoke definitely works via stdweb from Yew. I'll post an example soon.

mbuscemi commented 4 years ago

@Ante-dev @Boscop Here is an example of successfully executing external.invoke from Rust Yew, which in turn activates the WebView layer.

I'm still trying to work out how to get messages going the other direction. One thing I tried was to inline stdweb js! macro into the view HTML. That did not compile. My next thought was to do a #[js_export] on a model function. That did not compile either, as self is apparently not allowed in JS exports. Not sure what to explore next.

Boscop commented 4 years ago

@mbuscemi In your frontend (in your App::create method), you can register an event handler on document for a custom event that sends a Msg to your app. Then from the backend you execute js that dispatches this event on document.

https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent https://javascript.info/dispatch-events#bubbling-example

Let me know how it goes :)

mbuscemi commented 4 years ago

@Boscop If I use link.callback in App::create, does that translate to an event handler registered on the document, or is there a different syntax for that?

mbuscemi commented 4 years ago

It looks like virtual_dom::Listener::attach is probably what I want, but I can't find any examples on how to get access to a virtual dom element. https://docs.rs/yew-stdweb/0.14.0/yew_stdweb/virtual_dom/trait.Listener.html

mbuscemi commented 4 years ago

@Boscop Here is where I'm at this morning. I've set up a js! block in my App::create to do the document.addEventListener. Unfortunately, my callback isn't being passed in succesfully. I get this error:

error[E0277]: the trait bound `stdweb::private::Newtype<_, yew::callback::Callback<std::string::String>>: stdweb::private::JsSerializeOwned` is not satisfied
  --> src/lib.rs:42:21
   |
42 |           let value = js! {
   |  _____________________^
43 | |             var callback = @{set_file_callback};
44 | |             return document.addEventListener("set_file", content => alert(content));
45 | |         };
   | |_________^ the trait `stdweb::private::JsSerializeOwned` is not implemented for `stdweb::private::Newtype<_, yew::callback::Callback<std::string::String>>`
   |
   = help: the following implementations were found:
             <stdweb::private::Newtype<(stdweb::webcore::serialization::FunctionTag, ()), F> as stdweb::private::JsSerializeOwned>
             <stdweb::private::Newtype<(stdweb::webcore::serialization::FunctionTag, ()), std::option::Option<F>> as stdweb::private::JsSerializeOwned>
             <stdweb::private::Newtype<(stdweb::webcore::serialization::FunctionTag, ()), std::option::Option<stdweb::Mut<F>>> as stdweb::private::JsSerializeOwned>
             <stdweb::private::Newtype<(stdweb::webcore::serialization::FunctionTag, ()), std::option::Option<stdweb::Once<F>>> as stdweb::private::JsSerializeOwned>
           and 81 others
   = note: required by `stdweb::private::JsSerializeOwned::into_js_owned`
   = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

I found this article, but applying the ReferenceType derive to my Msg enum generates:

error: proc-macro derive panicked
  --> src/lib.rs:28:39
   |
28 | #[derive(Clone, Debug, PartialEq, Eq, ReferenceType)]
   |                                       ^^^^^^^^^^^^^
   |
   = help: message: Only tuple structures are supported!

So, presuming this is how I'm supposed to set up the event listener, I'm unclear how to proceed.

Boscop commented 4 years ago

You can do it with this: https://docs.rs/stdweb/0.4.20/stdweb/web/fn.document.html https://docs.rs/stdweb/0.4.20/stdweb/web/trait.IEventTarget.html#method.add_event_listener

mbuscemi commented 4 years ago

I've arrived at a working example of bi-directional communication.

I explored using document and add_event_listener on the Rust side extensively. There doesn't seem to be an implementation of CustomEvent in stdweb, and so the type of the event to pass into add_event_listener proved to be an impassable hurdle.

However, I was able to get this working by setting up a js! block in my App::create function that registers the event on document (source). I modified my call from the Webview layer (source), and it all worked. I read on this ticket that I need to drop variables I register in js! blocks, so I added that to my App::destroy.

I'll check with the maintainer of stdweb if they have any interest in an implementation of CustomEvent. I'd be happy to contribute it, and it would make this example a lot less error prone. As it stands, a slight typo in a JS variable name leads to an obscure error.

That said, as per the topic of this ticket, two-way communication between Webview and Yew achieved.

Boscop commented 4 years ago

Nice!

Btw:

So you could just store a Value in your model, that is a js object like { listener: event => set_file_callback(event.detail.contents), callback: set_file_callback } (returned by that js block that registers the listener) so this contains everything you need to clean up.

mbuscemi commented 4 years ago

Good to know. I'm curious, as I had assumed that all js! blocks would be contained within the same JS scope, so a var in one would be accessible to all the others. Apparently that's not the case?

My mind had already being going down a similar road as to your suggestion about the model. Mostly because (having already written large Elm apps), I'm thinking about how I want to organize the code when I have twenty or thirty or more of these communication points between Webview and Yew. I'll want a way to run through a vector or slice of them and initialize/destroy them all.

By the way, I experimented with loading up a file containing double quotes, and it loaded up just fine under the current implementation. Perhaps format! is taking care of that?

Thanks for your help!

Boscop commented 4 years ago

I'm curious, as I had assumed that all js! blocks would be contained within the same JS scope, so a var in one would be accessible to all the others. Apparently that's not the case?

Calling js!{} is like calling eval right then and there, it can't reference symbols from other js!{} blocks, unless those defined global vars and were called before (or returned values into Rust that you then pass into the other js block).

I experimented with loading up a file containing double quotes, and it loaded up just fine under the current implementation. Perhaps format! is taking care of that?

It's because you're putting single quotes around the contents. But it would fail with single quotes in contents..

mbuscemi commented 4 years ago

@Boscop How does this look for an implementation of cleaning up the callbacks?

Also, you were right about the strings. The OpenFile event was failing on files containing single quote characters. I fixed that, too.

Boscop commented 4 years ago

@mbuscemi Yeah it makes sense to factor the event stuff out.. But if I'm not completely mistaken: Each time you bring a rust closure into a js!{} scope it will allocate on the js side, so to be able to .drop() the right one, you need to keep a reference to it around (via a Value like in yew-geolocation). As it is now, since you're bringing the closure from rust to js in Event::destroy a second time, you're only dropping that instance that just got allocated in destroy. What I'd do here is:

            js! {
                var callback = @{js_callback};
                var name = @{name};
                var listener = event => callback(event.detail);
                document.addEventListener(name, listener);
                return {
                    callback: callback,
                    name: name,
                    listener: listener,
                };
            };

And then in Event::destroy, first call document.removeEventListener(handle.name, handle.listener); and then handle.callback.drop();

And some minor things:

mbuscemi commented 4 years ago

@Boscop Awesome suggestions!

Boscop commented 4 years ago

So, I'm pretty sure I was dropping the callback already. I saved it out a field on my event, which got saved to my Yew model, which I then used to invoke destroy.

Ah right, you had callback: Value in the model. For some reason I misread and thought you were storing the rust closure..

mbuscemi commented 4 years ago

Putting this here for anyone who might find this in the future: https://github.com/mbuscemi/webview-yew-minimal

hobofan commented 4 years ago

I also took a stab at it (taking a lot of inspiration from the discussed approach): https://github.com/hobofan/yew_webview_bridge (also available on crates.io)

A few notable points:

ohmree commented 3 years ago

I'd also like this.

Sadly I'm using seed and not yew so the premade solution won't work for me.

And while I was prepared to build a custom local storage backend (since localStorage doesn't work in data urls, which my embedded single page app uses) I don't really feel like delving into browser apis, seed's abstractions and where they intersect to build something that works :/

It'd be really great if the bind function from upstream could be implemented here, since at the moment there's no way for me to read stored values from my web app when it's embedded in a native wrapper and not running in a browser.