Loading repository data…
Loading repository data…
webliteca / repository
Java port of the Zserge Webview. Tiny cross-platform WebView
A cross-platform native WebView component for embedding in Java Swing applications. Java port of the tiny, light-weight WebView by Serge Zaitsev.
Add the dependency to your pom.xml:
<dependency>
<groupId>ca.weblite</groupId>
<artifactId>webview</artifactId>
<version>1.0.10</version>
</dependency>
The jar bundles the native libraries for macOS, Linux, and Windows — no additional native install step is required, with one exception:
| Platform | Heavyweight | Lightweight |
|---|---|---|
| macOS (Cocoa / WKWebView) | Full (rendering, input, resize, tab visibility) | Stub — falls back to default Swing background |
| Linux (WebKitGTK / X11) | Rendering, mouse, scroll, resize, tab switching work. Visible text-input feedback (caret blink, characters appearing as typed) is unreliable because of how GTK frame-clock and focus interact with XReparentWindow under a foreign (non-GTK) parent. | Full — rendering + mouse (click, drag, scroll, hover) + keyboard (typing, Backspace, Delete, arrows, function keys, common modifiers) |
| Windows (WebView2) | Full (rendering, input, resize, tab visibility) on Windows 11 | Stub |
The WebViewComponent.create() factory picks the right mode for the
current platform (heavyweight on macOS / Windows, lightweight on
Linux), so most callers don't need to think about it.
The standard platform shortcut (Cmd on macOS, Ctrl on Linux /
Windows) + C / V / X / A performs Copy / Paste / Cut /
Select-All inside the embedded WebView on all platforms. A
KeyEventDispatcher installed on the component routes the shortcut to
the native editing primitive — [WKWebView copy:/paste:/cut:/selectAll:]
on macOS, webkit_web_view_execute_editing_command on Linux,
document.execCommand on Windows. Sibling Swing widgets (a
JTextField in a toolbar above the WebView, etc.) keep their default
shortcut handling — the dispatcher only fires when the user is actually
interacting with the WebView.
import ca.weblite.webview.swing.WebViewComponent;
import javax.swing.*;
import java.awt.*;
public class Demo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
WebViewComponent wv = WebViewComponent.create();
wv.setUrl("https://example.com");
wv.setPreferredSize(new Dimension(900, 600));
JFrame frame = new JFrame("WebView Demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(wv, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
});
}
}
WebViewComponent.create() returns whichever implementation is best
for the current platform. Two concrete subclasses both extend
WebViewComponent:
WebViewHeavyweightComponent — embeds the native WebView as a
child of the underlying heavyweight AWT peer. Renders directly to
screen pixels. Native compositing means the highest fidelity and
lowest overhead, but it interacts with Swing Z-order the way every
heavyweight AWT component does — it paints above any overlapping
lightweight Swing components in the same window (see "Heavyweight
popup notes" below).WebViewLightweightComponent — renders the WebView into an
offscreen surface, ships the pixels to Java, and Swing paints them
into a regular JComponent. Composites cleanly with arbitrary Swing
widgets and Z-order. Higher per-frame cost than heavyweight; mouse
and keyboard input is forwarded from Swing.To force a specific mode, either set the ca.weblite.webview.mode
system property to heavyweight or lightweight (case-insensitive),
or call the factory explicitly:
import ca.weblite.webview.swing.WebViewComponent;
import ca.weblite.webview.swing.WebViewComponent.Mode;
WebViewComponent wv = WebViewComponent.create(Mode.HEAVYWEIGHT);
// or Mode.LIGHTWEIGHT
You can also instantiate WebViewHeavyweightComponent or
WebViewLightweightComponent directly if you need to.
When using WebViewHeavyweightComponent, native Swing popups
(JComboBox dropdowns, JMenu, tooltips) render behind the
WebView's heavyweight peer unless you opt into heavyweight popup mode
at app start:
JPopupMenu.setDefaultLightWeightPopupEnabled(false);
ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false);
This makes popups appear as real OS windows that sit above heavyweight peers. Lightweight mode does not need this.
The embedded WebView does not take ownership of the host application's event loop.
The lightweight component renders WebKit into a GtkOffscreenWindow,
snapshots cairo_image_surface_t pixels at ~30Hz into a
BufferedImage, and paints that into the JComponent via
paintComponent. AWT MouseEvents and KeyEvents are translated to
GdkEvents and injected via gtk_main_do_event. Notes:
é, ñ) work for
ASCII-Latin-1 layouts but not for IME-driven layouts.<select> dropdowns from inside the
page log a gdk_window_move_to_rect: assertion 'window->transient_for'
warning and don't visibly appear — WebKit tries to position them
relative to a toplevel that doesn't exist in our offscreen model.
Not fatal; just a missing piece of UI for those interactions.JComboBox and tooltips composite over the
WebView with their normal lightweight rendering.XReparentWindow.
A dedicated GTK pump thread drives the WebKitGTK main loop
independently of AWT's X11 event loop. A 60Hz g_timeout drives
the paint pipeline (the X11 GdkFrameClock won't pace itself on a
reparented popup that has no WM relationship). Requires
libwebkit2gtk-4.0-dev or libwebkit2gtk-4.1-dev plus libxt-dev
(JDK 8's jawt_md.h pulls in X11 Intrinsics).NSWindow.contentView (looked up through the layer
hierarchy from the JAWT windowLayer), so WebKit's CARemoteLayer
compositing engages and input dispatch goes through AppKit's normal
responder chain. All input works end-to-end.HWND is created under the AWT
canvas HWND and an ICoreWebView2Controller + ICoreWebView2 are
hosted inside it (modern stable WebView2 SDK). Each embedded
WebView runs on its own worker thread that pumps a private message
queue. WebView2LoaderStatic.lib is linked statically so we ship
just webview.dll, no separate WebView2Loader.dll. The system
WebView2 Runtime (part of Edge / Windows 11) provides the actual
Chromium binaries.The AWT focus chain and the native focus chain (AppKit responder / Win32 keyboard focus) are independent on these platforms, and the heavyweight WebView's native peer holds native focus in a way AWT doesn't observe. Two consequences are handled automatically:
JTextComponent's caret is hidden (visual cue that typing now lands
in the WebView). macOS hooks becomeFirstResponder on the
WKWebView via a runtime class swizzle; Windows hooks
ICoreWebView2Controller::add_GotFocus.FocusEvent.FOCUS_GAINED. On Windows we
additionally force Win32 keyboard focus back to the JFrame HWND
(cross-thread SetFocus via AttachThreadInput) so subsequent
keystrokes actually reach the Swing component — WebView2 otherwise
keeps Win32 focus on its child HWND and steals keystrokes.For debugging, set -Dca.weblite.webview.debugShortcut=true (Java
side) and WEBVIEW_DEBUG_SHORTCUT=1 (native side) to log the
dispatcher decisions and Win32 SetFocus calls.
The native embedding layer (src_c/webview_embed.cpp) is quiet by
default — a normal embed/launch prints no [webview-embed] lines to
stderr. Set DEBUG_WEBVIEW_EMBED=1 on the way in — for example
DEBUG_WEBVIEW_EMBED=1 java -jar your-app.jar — to restore the full
verbose trace: JAWT resolution, the JAWT_GetAWT version-mask that
succeeded, GTK reparenting, the WebKit load lifecycle, click/focus
grabs, the repaint timer, navigation, the per-frame draw#/frame-clock
instrumentation, and (on macOS) host-NSView discovery and
WKWebView subview attachment. Genuine error/failure conditions
(missing JAWT_GetAWT, dlopen/dlsym failures, a rejected JAWT
version mask, a JAWT_LOCK_ERROR, a non-X11 GdkWindow, a WebKit
load-failed, or the macOS layer-only-fallback warning) always print,
regardless of the flag. The Windows port
(windows/webview_embed.cc) already logs only on failure, so it has
no default chatter to silence.
This flag is read by the native library, so it only takes effect once you are running against a native build that includes it — the natives are produced by
build-{linux,mac,windows}.sh/ the CI release matrix rather than checked into the repo, so downstream consumers pick it up after the next native release.The macOS
ApplePersistenceIgnoreState: Existing state will not be touched …line is emitted by AppKit itself, not by this library, so it is unaffected byDEBUG_WEBVIEW_EMBED.
Four methods on WebViewComponent (and on the standalone WebView)
cover the JS-interop surface:
eval(String js) — fire-and-forget. Runs the snippet in the
current document; the return value is discarded. Use for side
effects (scrollTo, document.title = "...", click a hidden
button).evalAsync(String js): CompletableFuture<String> — round-trips
the snippet's result back to Java. The future resolves with the
JSON.stringify'd return value (undefined becomes "null";
returned Promises are awaited). JS-side failures
(synchronous throw, Promise rejection, JSON.stringify TypeError)
complete the future exceptionally with a
JavaScriptEvalException. The snippet runs inside an IIFE, so
use return to yield a value — a bare expression is not the
IIFE's return.addJavascriptCallback(String name, JavascriptCallback cb) —
exposes a fire-and-forget Java callback at window.<name>(arg)
for the page to call. The callback returns nothing to JS. Use when
the page initiates the conversation, or when a long-lived JS
subscription needs to push events to Java.addJavascriptFunction(String name, JavascriptFunction fn) —
exposes a value-returning Java function at window.<name>(arg).
In the page it returns a Promise: const r = await window.<name>(arg).
No JavaScript glue — the Java side is just a lambda. The library
runs the (synchronous) handler on a background thread, so it can
block safely without freezing the UI or deadlocking the engine UI
thread against the EDT — the reason this exists instead of a
synchronous, value-returning addJavascriptCallback. A
`Compl