docs

Porting from 1.4 to 2.0

Migrate an application from Ultralight 1.4 to 2.0.

On this page

You can update an application from 1.4 to 2.0 by focusing on the specific subsystems your code touches. Most CPU-rendered apps need only a handful of edits— the heavier work lands on GPU drivers, Overlay users, custom clipboards, JSHelpers users, and C embedders.

Custom FileSystem, FontLoader, Logger, and ThreadFactory implementations compile against 2.0 untouched. A custom Surface gains one required method, Scroll().

At a Glance

Build and Toolchain

📘 Two headers share a name now

The new generated <Ultralight/Config.h> (edition and feature macros, pulled in by every Ultralight header) is a different file from <Ultralight/platform/Config.h> (the Config struct you fill in). 1.4 had only the latter.

The generated header defines UL_HAS(x) for feature checks (eg, #if UL_HAS(MEDIA)) and UL_EDITION_AT_LEAST(x) for edition checks. These preprocessor gates remove declarations from Free-edition headers entirely, so referencing them causes a compile error instead of a runtime failure.

Behavior Changes

These changes affect how a rebuilt application runs without triggering compile errors.

Core API

GPUDriver Implementations

Custom GPU drivers require substantial updates for 2.0. See Implementing a GPUDriver to implement the complete driver interface.

The C ULGPUDriver struct mirrors these changes. Its create and update callbacks take the new parameters, ULGPUState includes the new blend and texture fields, and the struct adds a get_device_caps function pointer.

Windows and AppCore

Moving from Overlay to Panels

Overlay is removed and replaced by the panel layout system. A panel is the unit of web content, and the window manages input, focus, and painting for each panel automatically:

C++
///
/// 1.4: create an Overlay covering the window.
///
auto overlay = Overlay::Create(window, 1024, 768, 0, 0);
overlay->view()->LoadURL("file:///app.html");
C++
///
/// 2.0: a bare AddPanel() fills the whole window.
///
auto panel = window->AddPanel();
panel->view()->LoadURL("file:///app.html");

Overlay calls map to window panel operations as shown below. See Laying Out Panels to construct and arrange the layout tree.

1.4 2.0
Overlay::Create(window, view, x, y) window->AdoptPanel(view, options)
overlay->view() panel->view()
overlay->Hide() / Show() / is_hidden() panel->Hide() / panel->Show() / panel->is_hidden()
overlay->Focus() panel->Focus()
overlay->Unfocus() window->ClearFocus() (clears focus for all panels)
overlay->has_focus() window->focused_panel() (compare the handle)
overlay->width() / x() panel->device_bounds() for device pixels (matching 1.4) or panel->bounds() for logical pixels— both return the resolved layout and are read-only.
overlay->MoveTo() / Resize() Declare sizes in the panel's options— the window re-resolves the layout on every resize or DPI change. A panel you place freely belongs in the foreground layer with an anchor (see Floating Panels).
overlay->NeedsRepaint() No replacement. The window paints panels itself, so nothing needs to poll.

Window and Monitor Geometry

Window::width(), Window::height(), Monitor::width(), and Monitor::height() now return logical double pixels instead of device pixels (macOS monitors already used logical values). The method names haven't changed, so existing call sites still compile— but they return different values whenever the scale factor isn't 1.0. Window::Create(), MoveTo(), x(), and y() keep the same units, but now use double. Assigning those values to an int truncates them.

Device-pixel dimensions are now returned by device_width() and device_height() on both Window and Monitor. The conversions ScreenToPixels() and PixelsToScreen() are replaced by LogicalToDevice() and DeviceToLogical(). The old screen_width() and screen_height() functions are removed.

WindowListener Callbacks and WindowFlags

WindowFlags is now an enum class. For example, kWindowFlags_Titled | kWindowFlags_Resizable becomes WindowFlags::Titled | WindowFlags::Resizable. The C API enumerator names remain unchanged.

Mouse, keyboard, and scroll callbacks in WindowListener gained a leading Window* parameter, and OnResize() now receives logical double dimensions.

🚧 Mark your WindowListener overrides with override

A 1.4 OnKeyEvent(const KeyEvent&) override still compiles in 2.0— it declares a brand-new function and silently stops being called. The override keyword turns that into a compile error, so add it before you build.

Smaller AppCore Changes

Clipboard Implementations

Clipboard::ReadPlainText() and WritePlainText() are replaced by whole-payload methods so a single copy operation can transfer multiple formats (eg, text/plain and text/html) at once:

C++
///
/// 1.4: plain text only.
///
String ReadPlainText() override;
void WritePlainText(const String& text) override;
C++
///
/// 2.0: whole payloads, one entry per format.
///
RefPtr<ClipboardData> Read() override;
void Write(RefPtr<ClipboardData> data) override;

A text-only backend remains simple. Return ClipboardData::Create(text) from Read(). In Write(), write data->AsText() and skip unrecognized data types.

The C ULClipboard struct mirrors these changes. Every callback gained a leading void* user_data parameter, the struct gained a user_data field, and the read and write functions operate on the new ULClipboardData handle rather than ULString.

JSHelpers Is Removed

Replacing JSHelpers with the Typed Bridge

The <AppCore/JSHelpers.h> header is removed, along with all its declarations (including JSValue, JSObject, JSFunction, SetJSContext(), and BindJSCallback). The typed bridge in <Ultralight/js/API.h> replaces these helpers. Unlike JSHelpers, bindings registered through the typed bridge survive page navigation:

C++
///
/// 1.4: set a global context and re-bind on every page load.
///
auto lock = caller->LockJSContext();
SetJSContext(lock->ctx());
JSObject global = JSGlobalObject();
global["ShowMessage"] = BindJSCallback(&MyApp::ShowMessage);
C++
///
/// 2.0: bind once and attach to the View-- bindings survive navigation.
///
js::API api("myApp");
api["ShowMessage"] = js::Bind(this, &MyApp::ShowMessage);

if (api.AttachTo(view.get()))
  view->LoadURL("file:///app.html");

The page now calls myApp.ShowMessage() instead of a global ShowMessage().

Keep the js::API object alive (eg, as a member of your app) for as long as pages should have the bindings— destroying it detaches it from every View.

JSHelpers Pattern Map

Common JSHelpers patterns map to the 2.0 typed bridge as shown in this table. See About JavaScript Interop to register callbacks and export classes.

JSHelpers (1.4) ultralight::js (2.0)
SetJSContext(...) global context state Not needed (values hold their context)
global["fn"] = BindJSCallback(&C::Fn) api["fn"] = js::Bind(this, &C::Fn) then api.AttachTo(view.get())
Re-binding on every OnWindowObjectReady Not needed (bindings survive navigation)
JSValue / JSObject / JSArray js::Value (typed reads via To(), Maybe(), Or(), writes via operator[])
JSEval("...") js::Context(view.get()).Evaluate("...")
JSGlobalObject() js::Context(view.get()).GlobalObject()
JSFunction + operator() js::Value::Invoke() or fn(args...)
Manual args[0].ToNumber() conversions Typed parameters ([](double a) { ... })

Using JavaScriptCore Directly

The raw JavaScriptCore C API remains fully supported. You can obtain a context reference by calling View::LockJSContext().

The library's extensions to JavaScriptCore are purely additive. Because JSClassDefinition gained additional members, code using JavaScriptCore must be recompiled rather than relinked. See Using JavaScriptCore Directly to work with native contexts and low-level JavaScript values.

The C API

Every 1.4 callback setter gained a trailing destroy_user_data parameter. Passing NULL indicates there is nothing to free. Every callback registration added in 2.0 takes the same parameter.

The destroy callback runs once when the registration releases its user data, and it never runs while the callback itself is active. You must update every registration call site. See C API Conventions for user-data lifetime and ownership rules:

C
///
/// 1.4: callback plus user data.
///
ulWindowSetCloseCallback(window, OnClose, my_data);
C
///
/// 2.0: the same call with the new fourth parameter.
///
ulWindowSetCloseCallback(window, OnClose, my_data, NULL);

What's New

Beyond the changes above, 2.0 adds new subsystems that have no 1.4 counterpart. Each subsystem is documented in its own guide: