StageWebView is a webview library for Haxe on Windows, Linux, macOS, iOS and Android.
It exposes two public APIs:
webview.WebViewfor plain Haxe projectsopenfl.media.StageWebViewfor OpenFL projects that want AIR-style behavior
The native backend is vendored in this repository. hxcpp talks to it directly, HashLink uses a shipped stagewebview.hdll (Windows), and the OpenFL layer stays optional unless you actually import it. Lime builds the same engines into its own binaries for lime.ui.WebView, so a Lime application does not need this library's Haxe API at all.
Source: https://github.com/dimensionscapeOrg/stagewebview
| Platform | Engine | Glue | Status |
|---|---|---|---|
| Windows | WebView2 | vendored webview.h + project/src/WebViewBackend.cpp |
built and run in CI |
| Linux | WebKitGTK 4.1 (GTK 3) | vendored webview.h + project/src/gtk |
built and run on Ubuntu 24.04, X11 and Wayland (through XWayland); the engine library for frameworks ships for x86-64 and ARM64 |
| macOS | WKWebView | project/src/apple (Objective-C++) |
built and run in CI (macOS 14, arm64): native tests and both samples |
| iOS | WKWebView | project/src/apple (Objective-C++) |
built in CI for the simulator, through hxcpp and as a Lime app; not run |
| Android | android.webkit.WebView |
Haxe over lime.system.JNI + android/src |
built and run in CI on an Android 14 emulator (x86_64) |
Windows keeps the code paths it had before the other platforms were added. Every other platform implements the same WebViewBackend C++ class through a PlatformWebView engine (project/src/WebViewBackendPlatform.cpp), except Android, which implements the Haxe backend interface directly because the webview lives in Java.
Windows needs nothing shipped beside the application: webview.h finds the installed WebView2 Runtime through the registry. Windows 11 includes the runtime, and most Windows 10 machines have it with Edge. Where it is missing, WebView.isSupported is false.
Linux needs the GTK and WebKitGTK development files to build (sudo apt install pkg-config libgtk-3-dev libwebkit2gtk-4.1-dev); the build reads their flags from pkg-config. At run time it needs a display: an X server, Xvfb, or a Wayland session with XWayland.
Linux and Android deliver page events, messages, script values and PDF results only when the host calls WebView.processEvents(). The OpenFL facade does that every frame; plain Haxe hosts call it from their own loop. On Windows, macOS and iOS the host's event loop already does the same work.
This project is trying to solve two related problems without making either side awkward.
OpenFL projects should have a real StageWebView implementation with a familiar AIR-shaped API. Non-OpenFL projects should be able to use the same backend through webview.WebView without inheriting OpenFL concepts, source paths, or editor noise they never asked for.
The internal backend is kept separate on purpose. That makes the public API cleaner today, and it keeps the lower layer easier to move closer to Lime later if that ever becomes the right call.
- Vendored webview backend for Windows and Linux, WKWebView for macOS and iOS, and a Java host for Android
- Direct hxcpp bindings
- HashLink hdll support (Windows)
- Public
webview.WebViewAPI - Optional OpenFL
StageWebViewfacade - Hidden webviews (
hidden) andprintToPDF()on every platform - Dox-generated API docs
- Samples for basic embedding, live sites, JavaScript bridging, browser UI, docs browsing and PDF reports
- A release prebuilt
stagewebview.hdllforWindows64 project/Files.xmland a C++ API for frameworks that build the engines in
webview.WebView is the framework-agnostic API. You can embed it inside an existing native host window, or let it create and own a top-level native window of its own.
import webview.WebView;
var webView = new WebView(null, {
devTools: true
});
webView.onMessage = function (message:String):Void
{
trace("Page says: " + message);
};
webView.setWindowTitle("StageWebView Solo Deck");
webView.resize(1280, 720);
webView.loadHTML("<h1>Hello from plain Haxe</h1>");
webView.run();This library provides the webview backend. It does not try to be a full general-purpose windowing toolkit, but it is comfortable embedding into one or standing up a small top-level window when that is all you need.
The OpenFL API sits on top of the same backend and aims for AIR parity where that is practical on Windows.
import openfl.geom.Rectangle;
import openfl.media.StageWebView;
var webView = new StageWebView();
webView.stage = stage;
webView.viewPort = new Rectangle(0, 0, stage.stageWidth, stage.stageHeight);
webView.loadURL("https://www.openfl.org");Pages talk to the host through WebView2's window.chrome.webview API on every platform. Where there is no WebView2, the library installs an equivalent object before page scripts run, so one page works everywhere:
window.chrome.webview.addEventListener("message", function (event) {
console.log("Haxe says " + event.data);
});
window.chrome.webview.postMessage("ready");Messages are strings. The emulated bridge sends a non-string passed to postMessage as JSON; WebView2 itself drops it, since onMessage reads messages as strings.
On Android the bridge is written into the HTML for loadHTML(). For pages loaded by URL it is injected when the page starts loading, which can be after the page's first scripts have run.
evaluateJavaScript() runs a script in the page and reports its value as JSON:
webView.evaluateJavaScript("document.title", function (success:Bool, result:String):Void
{
trace(success ? haxe.Json.parse(result) : result);
});undefined gives null, and a promise is not waited for. What a value JSON has no form for gives depends on the engine: a function is {} on WebView2 and null elsewhere. A script that throws fails with the reason, except on Android and older WebView2 Runtimes, which give null.
printToPDF() saves the loaded page as a paginated PDF with the platform's own print engine. It works on a webview created with hidden, so reports can be produced without any visible window.
var webView = new WebView(null, {hidden: true});
webView.onLoad = function ():Void
{
webView.printToPDF("report.pdf", {marginTop: 0.5, marginBottom: 0.5}, function (success:Bool, error:String):Void
{
trace(success ? "saved" : error);
});
};
webView.loadHTML(html);| Platform | How | Notes |
|---|---|---|
| Windows | WebView2 PrintToPdf |
|
| Linux | WebKitPrintOperation to GTK's print-to-file printer |
|
| macOS | NSPrintOperation from printOperationWithPrintInfo:, run for an off-screen window |
createPDF makes one tall page, so it is not used |
| iOS | UIPrintPageRenderer with the web view's print formatter |
|
| Android | WebView.createPrintDocumentAdapter driven to a file |
uses the result callbacks' hidden constructors (android/src/android/print); scale and printBackground are not supported |
PDFOptions takes the paper size and margins in inches. The defaults are US Letter with 0.4 inch margins, which is what WebView2 uses on its own, so documents come out the same everywhere unless you say otherwise.
Things learned from the report sample:
- Chromium (Windows, Android) repeats a table's
<thead>on every printed page. WebKit (Linux, macOS, iOS) does not. WithrepeatTableHeaders(on by default) the library splits tables at the page boundaries before printing on WebKit, clones the header onto each part, and restores the page afterwards. It measures the screen layout, so styles that only exist under@media printare not taken into account. - WebKit lays print pages out at 90 CSS pixels per inch where Chromium uses 96, so the same template prints about 6% larger on Linux, macOS and iOS.
- Set
print-color-adjust: exactin templates as well asprintBackground; engines differ in which they honour.
The callback runs from the event loop, never inside the engine's own handler, so it may close the webview or open another.
The OpenFL facade places a native child over the stage at viewPort. It gets the stage's window from Lime's Window.nativeHandle; with a Lime from before 8.4.0, which does not have it, it finds the window by giving it a title nothing else has for a moment.
- Windows: a child
HWND. - Linux: a GTK popup window reparented into SDL's X11 window. Lime's SDL has no Wayland driver, so in a Wayland session the app runs on XWayland and the library asks GDK for X11 as well; embedding then works the same way. If GTK is forced onto Wayland (
GDK_BACKEND=wayland) there is no X11 window to embed into: hidden webviews and PDF output still work, and the facade dispatches anErrorEventinstead of showing the view. - macOS and iOS: a container view added over SDL's view, sized in points.
- Android: a
FrameLayoutadded over SDL's surface.
The engines know nothing of Haxe, so a framework can build them into its own binary, as Lime does with native-desktop. project/Files.xml defines the file groups, stagewebview and, for macOS and iOS, stagewebview-apple. project/include/stagewebview is the C++ API: WebViewBackend for the webview and WindowBackend.h for a child window to host it. Android's engine is Java, in android/src.
<include name="${haxelib:StageWebView}/project/Files.xml" />A binary that every program loads cannot link WebKitGTK, or a system without it could start none of them. On Linux such a framework builds the stagewebview-client group instead of stagewebview. It needs no GTK or WebKitGTK to build, and opens stagewebview-gtk.so, from ndll/Linux64 or ndll/LinuxArm64, the first time a webview is asked for, looking beside the binary, then beside the program, then wherever the system looks. Where the library or WebKitGTK is missing, WebViewBackend::isSupported() is false, and nothing else notices. project/build-gtk-engine.sh builds the library for the machine it runs on, which CI does on its oldest x86-64 and ARM64 runners.
Depending on this haxelib costs a program nothing unless it uses webview.WebView: the GTK flags and the HashLink runtime are only set up as that class's backend is built.
The published haxelib keeps classPath pointed at src, so pure Haxe projects only see webview.* by default.
When openfl is defined, extraParams.hxml adds src-openfl automatically. That makes openfl.media.StageWebView available for OpenFL users without dumping openfl.* completion noise into projects that do not use OpenFL at all.
The repository includes a few demo projects:
project.xml: minimal sampleproject-intermediate.xml: live-site demoproject-bridge.xml: JavaScript-to-Haxe bridge demoproject-browser.xml: browser-style sample with tabs and controlsproject-docs.xml: local viewer for the generated API docs, in the repository onlyproject-report.xml: renders a report in a hidden webview, fills it over the bridge, prints it to PDF and shows the filled report on stage; runs on every platformsamples/pure-haxe/build-cpp.hxml: standalonewebview.WebViewsample with its own native window, local HTML deck, bridge messaging, window resizing, and Haxelib search routingsamples/pdf/build-cpp.hxml: headless plain-Haxe PDF sample for Windows, Linux and macOS; exits 0 once the PDF is written
Generated sample output goes under bin.
The tests use utest (haxelib install utest). The unit suite runs on the interpreter with a fake backend:
haxe build/tests.hxmlThe native suite builds with hxcpp and drives the real platform webview through hidden instances. It covers page load events, titles, file navigation and history, init scripts and evaluateJavaScript(), the JavaScript bridge in both directions, and printToPDF() (page count, paper size, the page left unchanged afterwards, failure reporting, a callback that disposes its webview). On Linux it needs a display (xvfb-run works). CI runs it on Windows, Linux and macOS; Android is covered by running the report sample on an emulator.
haxe build/tests-native.hxml
bin/tests/native/NativeTestMainThe unit suite covers:
webview.WebViewstate and callback behavior through a fake backendopenfl.media.StageWebViewconstructor behavior, viewport validation, and offline state handlingprintToPDF()option defaults and result reporting- script delegation
CI (.github/workflows/ci.yml) builds every platform and, where the runner can show a webview, runs the PDF samples and checks the result with .github/scripts/check_report_pdf.py: two pages, the table header on both.
The docs are generated in two steps:
haxe build/docs-cpp.hxml
haxelib run dox -i docs/xml/cpp.xml -o docs/api --title StageWebView -in "^webview$" -in "^webview[.]" -ex "^webview[.]_internal" -in "^openfl$" -in "^openfl[.]media$" -in "^openfl[.]media[.]StageWebView$" -in "^openfl[.]events$" -in "^openfl[.]events[.](LocationChangeEvent|WebViewDrawEvent)$"The generated site ends up in docs/api.
webview.WebView was renamed to read like other Haxe APIs, and each thing now has one name:
| 0.7 | 1.0 |
|---|---|
loadURL(), navigate() |
load() |
loadString(), setHtml() |
loadHTML() |
historyBack(), historyForward() |
goBack(), goForward() |
canGoBack(), canGoForward(), isHistoryBackEnabled, isHistoryForwardEnabled |
canGoBack, canGoForward |
location, getLocation() |
url |
getTitle() |
title |
assignFocus() |
focus() |
dispose(), destroy() |
close() |
setSize(), setBounds() |
resize() |
setTitle() |
setWindowTitle() |
fromWindow() |
new WebView() |
onComplete |
onLoad |
onLocationChange |
onURLChange |
onLocationChanging |
onNavigate |
PdfOptions |
PDFOptions |
Options are now typed, so a misspelled one is a compile error:
| 0.7 | 1.0 |
|---|---|
debug, enableDevTools |
devTools |
enableContextMenu |
contextMenu |
enableKeyboardShortcuts |
keyboardShortcuts |
enableStatusBar |
statusBar |
enableZoom |
zoom |
startHidden |
hidden |
mediaPlaybackRequiresUserAction |
autoplay, the other way around |
The OpenFL facade keeps AIR's names, including mediaPlaybackRequiresUserAction, and takes the options above for the ones AIR does not have.
WebView.isSupportedis a property, read at run time, so it is no longer a constant. It isfalseon Windows without the WebView2 Runtime, and on targets with no backend, which an HTML5 build made on Windows used to get wrong.WebView2Loader.dlland theEBWebViewfolder are no longer shipped or copied beside the application. Nothing needs them, so remove them from any packaging that expects them. Applications still need the WebView2 Runtime, as they always did.evaluateJavaScript()can report the script's value, on every engine.- The OpenFL facade keeps developer tools off outside
-debugbuilds. Pass{devTools: true}to turn them on. onNavigateasks aboutload()on every engine, Android included, and no longer asks about the page fromloadHTML(), which goes nowhere.- A navigation cancelled through
onNavigate, or stopped withstop(), no longer reachesonError. - With a Lime that has
Window.nativeHandle, the OpenFL facade gets the stage's window from it, so the window's title no longer changes for a moment. - Internal classes moved from
_internal.webviewtowebview._internal. The native sources moved fromlibtoproject, andbuild/BuildNative.xmlandbuild/BuildHashlink.xmlare nowproject/Build.xmlandproject/BuildHashlink.xml. haxelib.jsonsaid MIT in error. The license has always been the Apache License 2.0.
StageWebView is under the Apache License 2.0, as LICENSE says. NOTICE lists the third-party code it includes.
- Windows, Linux, macOS, iOS and Android
- hxcpp everywhere; HashLink on Windows x64
- AIR-style
StageWebViewparity where the platform allows it - Public APIs kept library-owned, with the backend kept internal
