lune-webview
forked from naqvis/webviewCrystal Webview
Note: This repository is a fork of naqvis/webview maintained for the Lune framework. It carries a handful of additions on top of the upstream shard (see Lune fork additions below). All upstream features and documentation below are preserved as-is.
Crystal language bindings for zserge's Webview which is an excellent cross-platform single-header webview library for C/C++ using Gtk, Cocoa, or MSHTML/Edge, depending on the host OS.
Webview relies on default rendering engine of host Operating System, thus binaries generated with this Shard will be much more leaner as compared to Electron which bundles Chromium with each distribution.
This shard supports two-way bindings between Crystal and JavaScript. You can invoke JS code via Webview::Webview#eval and calling Crystal code from JS is done via WebView::Webview#bind (refer to Examples 3 & 4 for samples on how to invoke Crystal functions from JS).
Webview-supported platforms and the engines you can expect to render your application content are as follows:
| Operating System | Browser Engine Used |
|---|---|
| macOS | Cocoa, WebKit |
| Linux | GTK 3, WebKitGTK |
| Windows | Windows API, WebView2 |
Pre-requisite
If you're planning on targeting Linux or BSD you must ensure that WebKit2GTK is already installed and available for discovery via the pkg-config command.
Debian-based systems:
- Packages:
- Development:
apt install libgtk-3-dev libwebkit2gtk-4.1-dev - Production:
apt install libgtk-3-0 libwebkit2gtk-4.1-0
- Development:
BSD-based systems:
- FreeBSD packages:
pkg install webkit2-gtk3 - Execution on BSD-based systems may require adding the
wxallowedoption (see mount(8)) to your fstab to bypass W^X memory protection for your executable. Please see if it works without disabling this security feature first.
Microsoft Windows:
- You should have Visual C++ Build tools already as it's a pre-requisite for crystal compiler
git clone https://github.com/webview/webviewto get WebView sourceswebview\script\build.batto compile them (it will download required nuget package)- copy
webview\dll\x64\webview.libto<your crystal installation>\lib - copy
webview\dll\x64\webview.dllto directory with your program
Installation
-
Add the dependency to your
shard.yml:dependencies: webview: github: AristoRap/lune-webview branch: masterTo use the unmodified upstream shard instead, point at
naqvis/webview. -
Run
shards install
New Features
Type-Safe Bindings
Use bind_typed for compile-time type safety and automatic JSON conversion:
wv.bind_typed("add", Int32, Int32) do |a, b|
a + b # Clean, automatic conversion!
end
Instead of manual JSON handling:
wv.bind("add", Webview::JSProc.new { |args|
a = args[0].as_i.to_i32 # Manual conversion
b = args[1].as_i.to_i32
JSON::Any.new(a + b) # Manual wrapping
})
RAII Resource Management
Automatic cleanup with with_window:
Webview.with_window(800, 600, Webview::SizeHints::NONE, "My App") do |wv|
wv.html = "<h1>Hello</h1>"
wv.run
end # Automatically destroyed
Lifecycle Hooks
React to page events:
wv.on_load = -> { puts "Page loaded!" }
wv.on_navigate = ->(url : String) { puts "Navigating to #{url}" }
Async/Fiber Support
Non-blocking JavaScript evaluation:
wv.eval_async("console.log('Hello')") do
puts "JavaScript executed"
end
# Or with channels
channel = wv.eval_with_channel("someCode()")
channel.receive # Wait for completion
Multi-Window Management
Manage multiple windows easily:
Webview::WindowManager.with_manager do |manager|
window1 = manager.create_window(800, 600, Webview::SizeHints::NONE, "Window 1")
window2 = manager.create_window(800, 600, Webview::SizeHints::NONE, "Window 2")
# All windows automatically cleaned up
end
Better Error Handling
Errors now include context:
# Errors show what operation failed
wv.navigate("invalid://url") # Error: "navigating to invalid://url"
Native Handle Access
Access platform-specific handles:
window_handle = wv.window
ui_widget = wv.native_handle(Webview::NativeHandleKind::UI_WIDGET)
See the examples/ directory for complete working examples.
Lune fork additions
Beyond the upstream features listed above, this fork adds a small set of methods used by the Lune framework. All of them are no-op or generic enough to be useful in any consuming app — none of them depend on Lune itself.
bind_deferred — async bindings without blocking the webview thread
bind auto-resolves the JS Promise as soon as the Crystal callback returns. bind_deferred splits that into two steps: the callback receives a seq id; the caller calls resolve(seq, ...) whenever the result is ready. Useful when the binding's work is genuinely async — file I/O, network, a long-running computation on another fiber — so the webview thread isn't pinned waiting for it.
wv.bind_deferred("fetch") do |seq, args|
url = args[0].as_s
spawn do
response = HTTP::Client.get(url)
wv.resolve(seq, 0, response.body.to_json) # 0 = success
end
end
Malformed binding payloads (non-JSON or non-array) auto-reject the JS Promise with {"error": "webview: malformed binding payload: ..."} rather than crashing the C callback.
set_accel — Win32 menu-accelerator routing
Installs an HACCEL that TranslateAcceleratorW examines along two paths, covering both focus states:
run_impl's message pump — runsTranslateAcceleratorW(window, accel, &msg)beforeTranslateMessage/DispatchMessageW, for keystrokes that reach ourGetMessageW(i.e. when the top-level window has keyboard focus).ICoreWebView2Controller::AcceleratorKeyPressedevent — fires when the WV2 child has focus. WV2's input pipeline never queuesWM_KEYDOWNfor our pump in that case, so the handler synthesises aMSGfrom the event's virtual key and runsTranslateAcceleratorWon the same table. On a match it setsargs->put_Handled(TRUE)so WV2 doesn't act on the keystroke (e.g. open its print dialog) andWM_COMMANDis sent to the top-level window. Subscribed automatically insideembed()— embedder just callsset_accel.
wv.set_accel(haccel) # install
wv.set_accel(nil) # clear
Caller retains ownership of the HACCEL (call DestroyAcceleratorTable when done). The Crystal call is a no-op on macOS / Linux so cross-platform code can call it unconditionally. Pairs with set_browser_accelerator_keys_enabled(false) below to fully suppress WV2's built-in Ctrl+P / Ctrl+F / Ctrl+R defaults.
set_accel_target — re-route WM_COMMAND to another window
By default the AcceleratorKeyPressed handler dispatches WM_COMMAND to the webview's own top-level window (m_window). For a multi-window app where the main window owns the menu and command handlers, child-window webviews can call this to point their accelerators back at the main window's HWND — keystrokes hit the same wndproc/handler set regardless of which window has focus.
child_wv.set_accel_target(main_hwnd) # route to main
child_wv.set_accel(haccel) # install the same table
child_wv.set_accel_target(nil) # restore default (route to self)
Idempotent and cheap; combine with set_accel (the handler reads both s_accel_table and the per-instance target). No-op on non-Win32.
set_browser_accelerator_keys_enabled — suppress WV2 browser shortcuts
Toggles WebView2's built-in browser accelerators (Ctrl+P / Ctrl+F / Ctrl+R / Ctrl+- / Ctrl+0 / etc.). Default is true — WV2 handles these for its own UI (print dialog, find bar, reload). Setting it to false lets the keystrokes bubble up to the message pump, where set_accel's HACCEL can dispatch them to the embedder's menu.
wv.set_browser_accelerator_keys_enabled(false)
Routes through ICoreWebView2Settings3::put_AreBrowserAcceleratorKeysEnabled; needs WV2 to be ready, so call after the controller is created (typically right after Webview.with_window/Webview.new). No-op on non-Win32 builds.
This is the necessary companion to set_accel: without suppressing WV2's defaults, WV2 processes shortcuts like Ctrl+P in its own pipeline before TranslateAcceleratorW ever sees them, and the menu-routed HACCEL never fires.
Webview::WebviewLike — protocol for spec testability
A small abstract module that Webview::Webview natively includes. Lets downstream consumers (e.g. a bridge layer) type their parameters against the protocol so spec fakes can be stubbed without spinning up a real webview (with its C state, message pump, and platform window).
module Webview
module WebviewLike
abstract def bind_deferred(name : String, &block : String, Array(JSON::Any) -> Nil)
abstract def dispatch(&f : ->)
abstract def resolve(seq : String, status : Int32, result : String)
abstract def eval(js : String)
end
end
A spec fake that satisfies this protocol can stand in for a real Webview::Webview anywhere the consumer accepts Webview::WebviewLike.
Usage
Example 1: Loading URL
require "webview"
wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView", "http://crystal-lang.org")
wv.run
wv.destroy
Example 2: Loading HTML
require "webview"
html = <<-HTML
<!DOCTYPE html><html lang="en-US">
<head>
<title>Hello,World!</title>
</head>
<body>
<div class="container">
<header>
<!-- Logo -->
<h1>City Gallery</h1>
</header>
<nav>
<ul>
<li><a href="/London">London</a></li>
<li><a href="/Paris">Paris</a></li>
<li><a href="/Tokyo">Tokyo</a></li>
</ul>
</nav>
<article>
<h1>London</h1>
<img src="pic_mountain.jpg" alt="Mountain View" style="width:304px;height:228px;">
<p>London is the capital city of England. It is the most populous city in the United Kingdom, with a metropolitan area of over 13 million inhabitants.</p>
<p>Standing on the River Thames, London has been a major settlement for two millennia, its history going back to its founding by the Romans, who named it Londinium.</p>
</article>
<footer>Copyright © W3Schools.com</footer>
</div>
</body>
</html>
HTML
wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView")
wv.html = html
wv.run
wv.destroy
Example 3: Calling Crystal code from JavaScript
require "webview"
html = <<-HTML
<!doctype html>
<html>
<body>hello</body>
<script>
window.onload = function() {
document.body.innerText = "Javascript calling Crystal code";
noop().then(function(res) {
console.log('noop res', res);
add(1, 2).then(function(res) {
console.log('add res', res);
});
});
};
</script>
</html>
HTML
wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView", true)
wv.html = html
wv.bind("noop", Webview::JSProc.new { |a|
pp "Noop called with arguments: #{a}"
JSON::Any.new("noop")
})
wv.bind("add", Webview::JSProc.new { |a|
pp "add called with arguments: #{a}"
ret = 0_i64
a.each do |v|
ret += v.as_i64
end
JSON::Any.new(ret)
})
wv.run
wv.destroy
Example 4: Calling Crystal code from JavaScript and executing JavaScript from Crystal
require "webview"
html = <<-HTML
<!DOCTYPE html><html lang="en-US">
<head>
<title>Hello,World!</title>
</head>
<body>
<button onClick="add(document.body.children.length)">Add</button>
</body>
</html>
HTML
inject = <<-JS
elem = document.createElement('div');
elem.innerHTML = "hello webview %s";
document.body.appendChild(elem);
JS
wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView", true)
wv.html = html
wv.bind("add", Webview::JSProc.new { |n|
wv.eval(sprintf(inject, n))
JSON::Any.new(nil)
})
wv.run
wv.destroy
Example 5: Running your web app in another thread
Thread.new do
get "/" do
"hello from kemal"
end
Kemal.run
end
wv = Webview.window(640, 480, Webview::SizeHints::NONE, "WebView with local webapp!", "http://localhost:3000")
wv.run
wv.destroy
Example 6: Type-Safe Bindings (New!)
require "webview"
html = <<-HTML
<!DOCTYPE html>
<html>
<body>
<button onclick="testAdd()">Test Add</button>
<div id="result"></div>
<script>
async function testAdd() {
const result = await add(5, 3);
document.getElementById('result').textContent = 'Result: ' + result;
}
</script>
</body>
</html>
HTML
Webview.with_window(640, 480, Webview::SizeHints::NONE, "Type-Safe Demo") do |wv|
wv.html = html
# Type-safe binding - automatic conversion!
wv.bind_typed("add", Int32, Int32) do |a, b|
a + b
end
wv.run
end # Automatically destroyed
App Distribution
Distribution of your app is outside the scope of this library but we can give some pointers for you to explore.
macOS Application Bundle
On macOS you would typically create a bundle for your app with an icon and proper metadata.
A minimalistic bundle typically has the following directory structure:
example.app bundle
└── Contents
├── Info.plist information property list
├── MacOS
| └── example executable
└── Resources
└── example.icns icon
Read more about the structure of bundles at the Apple Developer site.
Tip: The
png2icnstool can create icns files from PNG files. See theicnsutilspackage for Debian-based systems.
Windows Apps
You would typically create a resource script file (*.rc) with information about the app as well as an icon. Since you should have MinGW-w64 readily available then you can compile the file using windres and link it into your program. If you instead use Visual C++ then look into the Windows Resource Compiler.
The directory structure could look like this:
my-project/
├── icons/
| ├── application.ico
| └── window.ico
├── basic.cc
└── resources.rc
resources.rc:
100 ICON "icons\\application.ico"
32512 ICON "icons\\window.ico"
Note: The ID of the icon resource to be used for the window must be
32512(IDI_APPLICATION).
Limitations
Browser Features
Since a browser engine is not a full web browser it may not support every feature you may expect from a browser. If you find that a feature does not work as expected then please consult with the browser engine's documentation and open an issue on webview library if you think that the library should support it.
For example, the webview library does not attempt to support user interaction features like alert(), confirm() and prompt() and other non-essential features like console.log().
Contributing
For changes that belong upstream (anything not specific to the Lune fork's additions), prefer opening the PR against the original repository: https://github.com/naqvis/webview/fork. The fork tracks upstream and will pick up merged changes.
For changes specific to the Lune fork (anything in Lune fork additions above, or the Win32 accelerator-routing patch):
- Fork this repository (https://github.com/AristoRap/lune-webview/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Ali Naqvi — creator and maintainer of the upstream shard
- Aristotelis Rapai — fork maintainer (Lune extensions)
lune-webview
- 0
- 0
- 0
- 1
- 0
- about 5 hours ago
- May 25, 2026
MIT License
Mon, 25 May 2026 18:41:46 GMT