[ANN] Webviews.jl - a tiny library for creating web-based desktop GUIs

Hi there,

I’d like to share with you Webviews.jl, a Julia implementation of webview for creating web-based desktop GUIs. Hope you find it interesting and useful.

Its usage is very simple:

# Julia v1.8+ on Windows/macOS or v1.9+ on Linux
using Webviews
w = Webview(320, 320, debug=true)
navigate!(w, "https://julialang.org")
run(w)

A new window with a given size will be created and a Webview is attached/embedded in it.
debug=true enables the dev tools, navigate! sets it to view Julia’s homepage and run(w) starts the GUI event loop. @async is used here so that it won’t block our REPL.

And then the window can be used like a normal web browser.

You can also directly give the Webview HTML content to show with html!:

# Since `run(w)` automatically destroys the previous one,
# a new webview instance should be created.
w = Webview()
html!(w, """<html><body>
<h1>Hello from Julia v$VERSION</h1>
<div id="data"></div>
</body></html>""")

Webviews.jl provides bind(func, webview, name) to bind a Julia function to a name with which you can invoke using JavaScript in the Webview.

bind(w, "add") do (a, b)
    a + b
end

JavaScript code can be executed with eval!

eval!(w, raw"""(async () => {
    const result = await add(1, 2)
    const div = document.getElementById("data")
    div.textContent = `result = ${result}`
})()""")
# You will see `result = 3` in the window.

Note that bound functions must take a Tuple as its only argument, and they are async functions (i.e., returns Promises) in JS.

Another common use case is to start an HTTP server as the backend to provide content, and simply call navigate!(w, "127.0.0.1:8000") if the HTTP server is listening on the 8000 port.

More examples can be found here.

It’s also worth mentioning that Webviews.jl directly uses libraries provided by the operating system, instead of JLL packages:

  • Latest Windows installs WebView2 by default.
  • macOS provides WebKit.
  • Most Linux distributions have a simple way to install WebKitGTK. (Yes, you have to install it manually, sorry.)

This way is intended since these libraries are native and efficient and Webviews.jl is just a GUI library that will probably not mess up with the backend program. Compared to other bindings to webview, it has the advantages of Julia’s dynamic features as well as its high performance.

PS: I kinda wanted to also develop a web-based GUI framework like tauri does for Rust, but it involves reinventing tools like those for windows creation and management on different platforms… So I haven’t started working on that.

Any feedback and comments are very welcome!

Best,
Sunoru

14 Likes

Just tried this, it seems nice! it might also be a good way to visualise plots made with a javascript backend lib (e.g. plotlyjs, vegalite) when not in an IDE.

A few questions:

  • how do you close a webview? I tried closing the window then re-starting the @async run(w) but that threw a Task (failed) error (Webview is already destroyed) .
  • is there a method to close a webview from within the REPL? so something like close(w) looks like destroy is that method; would it maybe make sense to extend Base.close here? ah I guess this might not work if you want to keep the distinction between terminate and destroy
  • would it be possible to add navigation arrows to the viewer? e.g. in your example with julialang, if you navigate to downloads then want to go back to the main page; you can do that by clicking the logo but it’d be nice to also have a button that takes you to the previous page?

Thanks so much for your interest!

  • The current APIs of Webviews.jl are designed as closely as what the original webview library does, but I’m considering changing some of it. Right now, a webview is not exactly a window, because you can initialize the webview to be embedded into a previously-created parent window by passing unsafe_window_handle in the constructor.
    So the original library doesn’t have close, and its terminate function means to end the webview instance instead of closing the window or ending the whole program. But actually, the original library also has many problems (e.g. terminate doesn’t work on macOS), that are resolved in our implementation.
    I think extending Base.close does make sense right now since the webview and window are closely related to each other. I’ll add it in a future version. Distinguishing closing a window and closing a webview might make sense if I decide to realize the idea in the PS above, though.

  • run(w) will automatically call destroy once the webview is terminated. So you cannot rerun a Webview instance. In fact, it is also recommended in the main context (instead of @async). I may also change the API in the future to make it more flexible to integrate with Julia’s event loop. One idea I have right now is to export something like run_one_iteration, but it’s also like reinventing tao/winit that handles event loops and window management.

  • For widgets like navigation arrows, since they should be part of the window instead of the webview, they won’t be included in this package, but maybe some extension package can be implemented in the future (still, like what I mentioned above, something similar to tauri). By the way, in this specific case, you can right-click the empty area in the website to go back/forward/reload/stop.

2 Likes