Skip to content

About

Cross-Platform WebView for Haxe

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

StageWebView

browser

StageWebView is a webview library for Haxe on Windows, Linux, macOS, iOS and Android.

It exposes two public APIs:

  • webview.WebView for plain Haxe projects
  • openfl.media.StageWebView for 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

Platforms

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.

Why it exists

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.

What is here now

  • 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.WebView API
  • Optional OpenFL StageWebView facade
  • Hidden webviews (hidden) and printToPDF() 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.hdll for Windows64
  • project/Files.xml and a C++ API for frameworks that build the engines in

webview.WebView

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.

openfl.media.StageWebView

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");

JavaScript bridge

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.

PDF output

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. With repeatTableHeaders (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 print are 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: exact in templates as well as printBackground; 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.

Embedding

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 an ErrorEvent instead of showing the view.
  • macOS and iOS: a container view added over SDL's view, sized in points.
  • Android: a FrameLayout added over SDL's surface.

Building it into a framework

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.

Classpath behavior

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.

Samples

The repository includes a few demo projects:

  • project.xml: minimal sample
  • project-intermediate.xml: live-site demo
  • project-bridge.xml: JavaScript-to-Haxe bridge demo
  • project-browser.xml: browser-style sample with tabs and controls
  • project-docs.xml: local viewer for the generated API docs, in the repository only
  • project-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 platform
  • samples/pure-haxe/build-cpp.hxml: standalone webview.WebView sample with its own native window, local HTML deck, bridge messaging, window resizing, and Haxelib search routing
  • samples/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.

Tests

The tests use utest (haxelib install utest). The unit suite runs on the interpreter with a fake backend:

haxe build/tests.hxml

The 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/NativeTestMain

The unit suite covers:

  • webview.WebView state and callback behavior through a fake backend
  • openfl.media.StageWebView constructor behavior, viewport validation, and offline state handling
  • printToPDF() 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.

Docs

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.

Upgrading from 0.7

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.isSupported is a property, read at run time, so it is no longer a constant. It is false on Windows without the WebView2 Runtime, and on targets with no backend, which an HTML5 build made on Windows used to get wrong.
  • WebView2Loader.dll and the EBWebView folder 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 -debug builds. Pass {devTools: true} to turn them on.
  • onNavigate asks about load() on every engine, Android included, and no longer asks about the page from loadHTML(), which goes nowhere.
  • A navigation cancelled through onNavigate, or stopped with stop(), no longer reaches onError.
  • 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.webview to webview._internal. The native sources moved from lib to project, and build/BuildNative.xml and build/BuildHashlink.xml are now project/Build.xml and project/BuildHashlink.xml.
  • haxelib.json said MIT in error. The license has always been the Apache License 2.0.

License

StageWebView is under the Apache License 2.0, as LICENSE says. NOTICE lists the third-party code it includes.

Scope

  • Windows, Linux, macOS, iOS and Android
  • hxcpp everywhere; HashLink on Windows x64
  • AIR-style StageWebView parity where the platform allows it
  • Public APIs kept library-owned, with the backend kept internal

About

Cross-Platform WebView for Haxe

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages