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

- **Everyone** — recompile everything against the 2.0 SDK (the ABI changed) and skim [Behavior Changes](#content-behavior-changes).
- **AppCore apps** — `Overlay` is gone (the panel layout replaces it), window geometry moved to logical pixels, and `WindowListener` signatures changed.
- **GPUDriver implementers** — plan to update your entire driver for seven shader programs, new blend state, and new texture flags.
- **Custom Clipboard implementers** — the interface is replaced with whole-payload methods.
- **Custom Surface implementers** — add one required method, `Scroll()`. The dirty area is also available as a list of rectangles.
- **JSHelpers users** — the header is removed, replaced by the typed bridge.
- **C embedders** — every callback setter gained a fourth argument, and a few signatures changed.

## Build and Toolchain

- The public headers now require C++20— compiling with an older compiler or language mode triggers a `#error`. On Windows, you'll need Visual Studio 2022 or later with `/std:c++20`.
- The SDK ships as a CMake package. Use `find_package(Ultralight REQUIRED)`, link `Ultralight::AppCore`, and call `ultralight_copy_runtime_files()` to stage the runtime files. See [Linking to the Library](/docs/2.0/linking-to-the-library) to configure your build targets.
- The package layout is flat. Libraries live in `bin/` and `lib/` with no per-OS subfolders, and reference platform implementations (GPU drivers, font loaders, and file systems that back AppCore) ship as Zlib-licensed source in the package's **platform** folder.

> 📘 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.

- **Text renders differently** — Kerning and ligatures now apply (1.4 never shaped text), and a font profile controls glyph weight and hinting (`Config::font_gamma` now defaults to `0`, which inherits from `Config::font_profile`). Under AppCore, generic CSS font families come from the host OS (`monospace` resolves to Menlo on macOS and Consolas on Windows, sized at 13px). Metrics land much closer to mainstream browsers. See [Text Rendering and Fonts](/docs/2.0/text-rendering-and-fonts) to configure font profiles and the generic font families.
- **GPU-rendered Views use analytic rendering by default** — Paths, strokes, and text now render more sharply. Set `Config::enable_photon` and `Config::enable_photon_text` to `false` to keep the 1.4 look.
- **Submitting an invalid form now blocks** — Constraint validation runs the way browsers do. The `invalid` event fires, and the first invalid control takes focus. Calling `reportValidity()` checks the form without drawing a message bubble. Add `novalidate` to a form to submit without validation. See [Forms and Inputs](/docs/2.0/forms-and-inputs) to handle form validation and submission.
- **The compositor is enabled by default** — `ViewConfig::enable_compositor` defaults to `true` (was `false`). Elements with 3D or animated transforms, animated opacity, video, `will-change`, or `translateZ(0)` now render into their own layers— each layer uses extra memory.
- **WebAssembly is no longer available** — The global `WebAssembly` object is now undefined on all platforms (1.4 supported it on macOS and Linux, but never on Windows). Pages that check for the feature switch to their JavaScript fallback— pages that depend on it stop working.
- **Two `ViewConfig` defaults changed** — `ViewConfig::preferred_color_scheme` now defaults to `ColorScheme::Auto`, so AppCore apps match the OS dark mode and apply page dark styles (apps without AppCore are unaffected, and `ColorScheme::Light` keeps the 1.4 light look). `ViewConfig::clipboard_read_policy` now lets page script in local content (`file:///` pages and HTML from `View::LoadHTML()`) read the clipboard on a click or key press, excluding remote web pages. See [Dark Mode and Color Schemes](/docs/2.0/dark-mode-and-color-schemes) to configure theme settings and color schemes.
- **Editing follows the OS under AppCore** — `Settings::match_native_editing_behavior` defaults to `true`, so selection and caret behavior match each platform. Views you create manually keep uniform cross-platform editing unless you enable `ViewConfig::match_native_editing_behavior`.
- **Memory management runs automatically** — `Config::recycle_delay` sets the interval between automatic lightweight recycles and defaults to `0.5` seconds (was `4.0` seconds), with `0` disabling them. Idle pages reclaim JavaScript heaps automatically (`Config::idle_gc_enabled`), and `Config::memory_cache_size` defaults to 256 MiB (was 64 MiB). If you target memory-constrained devices, configure these values explicitly. See [Managing Memory](/docs/2.0/managing-memory) to tune memory profiles, cache limits, and reclamation.
- **The default User-Agent reports the host platform and `Ultralight/2.0.0`** — Version 1.4 reported Windows on every OS, so sites that sniff the User-Agent string may serve different content on macOS and Linux. Overriding `ViewConfig::user_agent` is unavailable in the Free edition.
- **Smooth scrolling uses a physics-based spring model** — The rewritten scrolling logic changes scroll feel to match macOS and iOS.
- **CSS transitions take precedence over `!important` declarations** — This change aligns with the CSS specification. Pages where transition targets use `!important` animate instead of snapping instantly.
- **`LogLevel` gained `Fatal` and `Debug`** — `Fatal` comes first and `Debug` last, so every level's number changed (in C, `kLogLevel_Error` is now `1`). Update `switch` statements in a custom `Logger` and any level numbers hard-coded in C or C# code. AppCore's default logger shows a message box for every `LogLevel::Fatal` message.

## Core API

- `RefPtr`'s boolean conversion is now an `explicit operator bool`. Conditional checks like `if (ptr)` still compile, but implicit conversions (such as `bool ok = ptr;` or passing a `RefPtr` to a `bool` parameter) require `ptr.get() != nullptr` instead.
- The `Config::animation_timer_delay` and `Config::scroll_timer_delay` fields are removed. Animations advance when you call `Renderer::RefreshDisplay()`. To throttle an individual View, set `ViewConfig::max_render_fps` or call `View::set_max_render_fps()` (`0` leaves the View unthrottled).
- `KeyEvent` now owns platform data— zeroing or copying it with `memset` or `memcpy` causes a crash. Construct and copy instances using standard C++ constructors and assignment.
- `BitmapFormat` gained new values, so update `switch` statements to handle the new cases. Compressed formats don't have a bytes-per-pixel value— call `GetBitmapFormatInfo()` to get format details.
- `View::LockJSContext()`, `View::JavaScriptVM()`, and `View::EvaluateScript()` each take an optional frame name to target an `<iframe>`. The parameter defaults to the main frame, so existing call sites compile unchanged.
- `Renderer::Recycle()` reclaims memory on demand, and `Renderer::PostTask()` schedules work on the renderer thread from any thread.
- Custom `Surface` implementations gain one required method, `Scroll()`, which shifts a rectangle of pixels when the page scrolls (most implementations are one call to `Surface::ShiftPixels()`, returning `false`). The dirty area is also available as a list of rectangles (`dirty_rect_count()`, `dirty_rect()`). `dirty_bounds()` is still their union, so existing display code keeps working. In C, `ULSurfaceDefinition` gained a `scroll` field appended last— zero-initialize the struct, and the library handles a `NULL` callback itself. See [Render Surfaces](/docs/2.0/using-a-custom-surface) to implement scrolling and handle dirty rectangles.

## GPUDriver Implementations

Custom GPU drivers require substantial updates for 2.0. See [Implementing a GPUDriver](/docs/2.0/implementing-a-gpudriver) to implement the complete driver interface.

- Seven shader programs are required instead of two. `FilterBasic`, `FilterBlur`, `FilterDropShadow`, `FillPhoton`, and `FillPhotonGrid` join `Fill` and `FillPath`. All programs are mandatory because the library never queries for per-program support. Stock shaders for every program ship in the SDK's **platform/shaders** folder in compiled or generated form for each backend.
- `GPUDriver::CreateTexture()` takes a third `flags` parameter (`kGPUTextureFlag_RenderTarget`, `kGPUTextureFlag_Antialiased`, and `kGPUTextureFlag_Immutable`). Texture flags now determine whether a texture is a render target, rather than checking for an empty bitmap.
- `GPUDriver::UpdateTexture()` takes a `dirty_rect` parameter as an optimization hint. You can upload only that region, or ignore the hint and upload the entire bitmap.
- `GPUState` gained `blend_src_factor`, `blend_dst_factor`, `blend_equation`, `texture_4_id`, and `uniform_integer`. Because the struct's byte layout changed, re-mirror your constant buffers and recompile. Map the blend fields directly to your graphics API's blend state— the renderer emits explicit blend state per draw instead of assuming premultiplied alpha.
- `VertexBufferFormat` gained a third layout, `_2f_2ui`, with the matching `Vertex_2f_2ui` struct (`float pos[2]`, `uint32_t header_addr`, `uint32_t path_slot`). The `FillPhotonGrid` program draws with it, so a driver that hardcodes two input layouts needs a third.
- `CommandType::Flush` is new. Submit pending GPU work before executing the next command (`Flush()` on Direct3D 11 or `glFlush()` on OpenGL, and typically a no-op on Direct3D 12, Metal, or Vulkan where pipeline barriers order accesses).
- `GPUDriver::GetDeviceCaps()` is new and optional as the driver's only non-pure virtual method. Override it to report MSAA support, compressed texture formats, and size limits, or omit it to let the engine substitute conservative defaults.
- Render targets can now be single-channel. Back an `A8_UNORM` render target with a renderable one-channel format (such as R8 on modern graphics APIs) and skip the alpha swizzle that sampled A8 textures use.

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:

```cpp
///
/// 1.4: create an Overlay covering the window.
///
auto overlay = Overlay::Create(window, 1024, 768, 0, 0);
overlay->view()->LoadURL("file:///app.html");
```

```cpp
///
/// 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](/docs/2.0/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](/docs/2.0/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

- `Settings::load_shaders_from_file_system` and its C setter are removed with no replacement.
- `App::RunOnce()` is new. It executes a single iteration of the application event loop, allowing you to run AppCore from an existing main loop instead of passing thread control to `Run()`.
- The 1.4 public AppCore source repository (LGPL) is superseded. Reference implementations previously copied from it (GPU drivers, font loaders, file systems, and clipboards) now ship as Zlib-licensed source in the SDK's **platform** folder.

## 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:

```cpp
///
/// 1.4: plain text only.
///
String ReadPlainText() override;
void WritePlainText(const String& text) override;
```

```cpp
///
/// 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:

```cpp
///
/// 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);
```

```cpp
///
/// 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](/docs/2.0/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](/docs/2.0/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](/docs/2.0/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);
```

- The Overlay C API (the `ULOverlay` handle and its associated functions) is removed in favor of the layout API. Call `ulWindowAddPanel(window, NULL, NULL)` to create a full-window panel, and call `ulPanelGetView()` to access its View. Layout functions are declared in `<AppCore/CAPI/CAPI_Layout.h>`.
- `ulCreateScrollEvent()` gained two cursor-coordinate parameters. Pass `-1, -1` to preserve 1.4 behavior and dispatch at the last known cursor position.
- `ulConfigSetAnimationTimerDelay()` and `ulConfigSetScrollTimerDelay()` are removed. Throttle individual views with `ulViewSetMaxRenderFps()`.
- Window and monitor geometry moved to logical `double` pixels. `ulWindowGetWidth()` and `ulMonitorGetWidth()` return logical pixels, while `ulWindowGetDeviceWidth()` provides the device-pixel measurement. `ULResizeCallback` passes `double` values, and screen-coordinate functions are removed.
- The C API also gained window-level keyboard, mouse, and scroll callbacks (1.4 had no C way to intercept them), native message boxes, `ulAppRunOnce()`, and dedicated setters for every `Config` field.

## 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:

- A [native DOM API](/docs/2.0/about-the-dom-api) and [declarative data bindings](/docs/2.0/about-data-bindings) to inspect, modify, and bind UI state without JavaScript.
- The [typed JavaScript bridge](/docs/2.0/about-javascript-interop) shown above to expose native functions and classes.
- [Text editing, IME, and clipboard control](/docs/2.0/text-editing-and-clipboard), including `View::editor()`.
- HTML5 [video and audio](/docs/2.0/media-and-audio) (Pro), [compressed GPU textures](/docs/2.0/compressed-textures) (Pro), the [Profiler](/docs/2.0/profiling-and-tracing) (Pro), and render tracing (Enterprise).
- [Handling View Events](/docs/2.0/handling-view-events), [Sessions and Site Data](/docs/2.0/sessions-and-site-data), [Dark Mode and Color Schemes](/docs/2.0/dark-mode-and-color-schemes), and the `Color`, `URL`, and `JSON` value types in [Working with Colors](/docs/2.0/working-with-colors), [Working with URLs](/docs/2.0/working-with-urls), and [Working with JSON](/docs/2.0/working-with-json).
- Window [backdrops, custom chrome, transparency](/docs/2.0/native-window-styling), and [popups](/docs/2.0/popups-and-dialogs) under AppCore.
- Frame timing is under your control through the timed form of `Renderer::RefreshDisplay()`, declared display refresh rates, and custom clocks. See [Updating and Rendering](/docs/2.0/updating-and-rendering) to configure custom animation timing. AppCore syncs animations to each frame's on-screen time for you and enumerates monitors through `App::monitor()`.
