lune-webview

forked from naqvis/webview
Crystal bindings to Webview library

Linux CI MacOSX CI Windows CI

Crystal 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

BSD-based systems:

  • FreeBSD packages: pkg install webkit2-gtk3
  • Execution on BSD-based systems may require adding the wxallowed option (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/webview to get WebView sources
  • webview\script\build.bat to compile them (it will download required nuget package)
  • copy webview\dll\x64\webview.lib to <your crystal installation>\lib
  • copy webview\dll\x64\webview.dll to directory with your program

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      webview:
        github: AristoRap/lune-webview
        branch: master
    

    To use the unmodified upstream shard instead, point at naqvis/webview.

  2. 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 — runs TranslateAcceleratorW(window, accel, &msg) before TranslateMessage/DispatchMessageW, for keystrokes that reach our GetMessageW (i.e. when the top-level window has keyboard focus).
  • ICoreWebView2Controller::AcceleratorKeyPressed event — fires when the WV2 child has focus. WV2's input pipeline never queues WM_KEYDOWN for our pump in that case, so the handler synthesises a MSG from the event's virtual key and runs TranslateAcceleratorW on the same table. On a match it sets args->put_Handled(TRUE) so WV2 doesn't act on the keystroke (e.g. open its print dialog) and WM_COMMAND is sent to the top-level window. Subscribed automatically inside embed() — embedder just calls set_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 &copy; 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 png2icns tool can create icns files from PNG files. See the icnsutils package 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):

  1. Fork this repository (https://github.com/AristoRap/lune-webview/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

Repository

lune-webview

Owner
Statistic
  • 0
  • 0
  • 0
  • 1
  • 0
  • about 5 hours ago
  • May 25, 2026
License

MIT License

Links
Synced at

Mon, 25 May 2026 18:41:46 GMT

Languages