docs

Views in C

Create Views, register callbacks, read pixels, and dispatch input in C.

On this page

You can create and manage Views in C to display web pages, receive lifecycle callbacks, inspect rendered pixels, and route input from your application. C++ remains the primary API— this page covers what differs in C, supplementing Creating Views and the input and editing guides.

General conventions for memory ownership, callback user data, and threading follow C API Conventions. To set up the renderer and manage sessions, see Renderer in C.

Differences from C++

The C API replaces C++ View objects, listener classes, and event structs with opaque handles and callback functions.

C++ Feature C API Equivalent How It Differs
ViewConfig struct ULViewConfig and ulViewConfigSet*() An opaque configuration handle you create and destroy.
RefPtr<View> ULView You must destroy the View handle manually with ulDestroyView().
Listener classes and set_*_listener() ulViewSet*Callback() Each event uses a separate callback function pointer.
OnCreateChildView() returning RefPtr<View> ULCreateChildViewCallback returning ULView You return a created handle that you continue to own.
View::url() and View::title() ulViewGetURL() and ulViewGetTitle() Return borrowed strings that subsequent calls overwrite (see C API Conventions).
NetworkRequest& ULString handle in ULNetworkRequestCallback Receives only the request URL (public key pinning is C++ only).
Cast to BitmapSurface*, then bitmap() ulBitmapSurfaceGetBitmap() Retrieves the underlying bitmap directly without casting.
LockPixelsSafe() guard ulSurfaceLockPixels() and ulSurfaceUnlockPixels() You must lock and unlock the pixel buffer manually.
Event structs ulCreate*Event() and ulDestroy*Event() You allocate event handles and destroy them after dispatch.

Creating a View

To create a View, call ulCreateView() with the renderer, the width and height in pixels, an optional configuration handle, and an optional session.

Passing NULL for the configuration uses default settings, and passing NULL for the session uses the default session.

The renderer copies the configuration settings during creation— you can destroy the ULViewConfig handle with ulDestroyViewConfig() immediately after ulCreateView() returns.

For a complete setup walkthrough, see the C tabs in Your First Game UI. For available configuration options, see Creating Views.

When you finish with a View you created, release it by calling ulDestroyView(). For how destroying a View you created differs from destroying a handle returned by ulPanelGetView(), see C API Conventions.

Registering Callbacks

Instead of listener classes, you register an individual callback function for each event on ULView.

Each setter accepts a function pointer, a user_data pointer, and an optional cleanup function to free that user data. For ownership rules and cleanup details, see C API Conventions.

A new View starts with no callbacks registered. Register your callbacks before loading content so you don't miss initial load events.

C++ Listener C API Setters Guide
LoadListener ulViewSetBeginLoadingCallback(), ulViewSetDOMReadyCallback(), and other load setters Handling View Events
ViewListener ulViewSetChangeTitleCallback(), ulViewSetCreateChildViewCallback(), ulViewSetRequestCloseCallback(), and other View setters (URL, tooltip, cursor, console message, and inspector View) Handling View Events
DownloadListener Six ulViewSetDownload*Callback() setters Downloads and Network Control
NetworkListener ulViewSetNetworkRequestCallback() Downloads and Network Control
EditorListener ulViewSetChangeEditableStateCallback(), ulViewSetUpdateCompositionCallback(), and ulViewSetDiscardCompositionCallback() Input Method Editors

Managing Callbacks Across Handles

Here is how callbacks work across View handles:

Opening Child Windows

When a page requests a new window, the library calls the callback registered with ulViewSetCreateChildViewCallback(). For when this fires and underlying concepts, see Handling View Events.

To allow the window, create a new View with ulCreateView(), display it, and return its handle. Returning NULL blocks the window.

The returned handle stays yours— you must keep it alive while the window remains open. Destroying the handle with ulDestroyView() inside your close callback releases the View safely.

Here is how to track child Views in a fixed array:

C
#include <Ultralight/CAPI.h>

#define MAX_CHILD_VIEWS 8
static ULView child_views[MAX_CHILD_VIEWS];

static void OnRequestClose(void* user_data, ULView caller) {
  ULView* slot = user_data;

  // Pseudo-code, close the window that shows this View here.
  HideChildView(caller);

  // Destroying our handle destroys the View.
  ulDestroyView(*slot);
  *slot = NULL;
}

static ULView OnCreateChildView(void* user_data, ULView caller,
                                ULString opener_url, ULString target_url,
                                bool is_popup, ULIntRect popup_rect) {
  (void)user_data, (void)opener_url, (void)target_url;
  (void)is_popup, (void)popup_rect;

  ULView* slot = NULL;
  for (int i = 0; i < MAX_CHILD_VIEWS && !slot; ++i) {
    if (!child_views[i])
      slot = &child_views[i];
  }
  if (!slot)
    return NULL;  // Blocks the window.

  // Match the renderer and device scale of the View that asked.
  ULViewConfig config = ulCreateViewConfig();
  ulViewConfigSetIsAccelerated(config, ulViewIsAccelerated(caller));
  ulViewConfigSetInitialDeviceScale(config, ulViewGetDeviceScale(caller));
  *slot = ulCreateView(renderer, 800, 600, config, NULL);
  ulDestroyViewConfig(config);

  // A new View has no callbacks; set ours to hear its close request.
  ulViewSetRequestCloseCallback(*slot, OnRequestClose, slot, NULL);

  // Pseudo-code, display the new View in your UI here.
  ShowChildView(*slot);

  return *slot;
}

Register the callback on the parent View with ulViewSetCreateChildViewCallback().

Handling Downloads and Network Requests

The renderer ignores file downloads until you register the download callbacks. For the complete download lifecycle, see Downloads and Network Control.

🚧 Set All Six Download Callbacks

You must register all six download callbacks together. Leaving the request callback unset blocks every download, while leaving the next-id callback unset assigns an ID of 0 to every download.

Keep these points in mind when handling downloads and network requests:

Reading Pixels

You can read pixels from CPU-rendered Views or retrieve render targets for GPU-accelerated Views.

Surface Pixels and Dirty Rectangles

Call ulViewGetSurface() to retrieve the ULSurface handle, lock it with ulSurfaceLockPixels(), copy the pixel data, and unlock it with ulSurfaceUnlockPixels(). Always stride across rows using ulSurfaceGetRowBytes(), as the row byte count can exceed width * 4.

If you prefer copying the single bounding box enclosing all updates instead of individual rectangles, see the C tab in Updating and Rendering.

To minimize copied pixels, lock the surface and copy each dirty rectangle:

C
#include <Ultralight/CAPI.h>

void UploadDirtyRects(ULView view) {
  // NULL for a GPU View.
  ULSurface surface = ulViewGetSurface(view);
  if (!surface)
    return;

  unsigned int count = ulSurfaceGetDirtyRectCount(surface);
  if (count == 0)
    return;

  void* pixels = ulSurfaceLockPixels(surface);
  unsigned int row_bytes = ulSurfaceGetRowBytes(surface);
  for (unsigned int i = 0; i < count; ++i) {
    // Pseudo-code, copy this rectangle into your texture.
    CopyRectToTexture(pixels, row_bytes, ulSurfaceGetDirtyRect(surface, i));
  }
  ulSurfaceUnlockPixels(surface);

  ulSurfaceClearDirtyBounds(surface);
}

Bitmaps and Custom Surfaces

To access the underlying bitmap of a default surface, pass the surface to ulBitmapSurfaceGetBitmap(). The returned bitmap handle is borrowed and owned by the surface— do not destroy it.

When using a custom surface definition, ulBitmapSurfaceGetBitmap() returns NULL. Call ulSurfaceGetUserData() to retrieve the pointer returned by your surface creation hook. For details, see Platform Handlers in C.

GPU Render Targets

For GPU-accelerated Views, ulViewGetSurface() returns NULL. Call ulViewGetRenderTarget() to obtain the render target struct by value, which provides the texture and frame buffer IDs used by your driver. For setup details, see GPU Driver in C.

Using Image Sources

You can display native pixel buffers inside web pages by creating image sources and registering them with the image source provider. Pages load the image through a .imgsrc file matching the registered identifier, as explained in Displaying Custom Textures.

To register a bitmap and release the temporary handles:

C
#include <Ultralight/CAPI.h>

void AddPortrait(void) {
  ULBitmap bitmap =
      ulCreateBitmap(128, 128, kBitmapFormat_BGRA8_UNORM_SRGB);

  // Pseudo-code, fill the bitmap's pixels here.
  DrawPortrait(bitmap);

  // Pages show it through a portrait.imgsrc file holding this id.
  ULImageSource source = ulCreateImageSourceFromBitmap(bitmap);
  ULString id = ulCreateString("portrait");
  ulImageSourceProviderAddImageSource(id, source);
  ulDestroyString(id);

  // The image source holds the bitmap and the provider holds the image
  // source, so both of our handles can go.
  ulDestroyImageSource(source);
  ulDestroyBitmap(bitmap);
}

Keeping Handles and Buffers

To update an image after redrawing it, keep both the ULBitmap and ULImageSource handles instead of destroying them. After drawing new pixel data into the bitmap, call ulImageSourceInvalidate() to request a repaint.

When creating a bitmap with ulCreateBitmapFromPixels() and should_copy set to false, the bitmap wraps your pixel buffer directly. The C API provides no release callback for wrapped memory— you must keep the buffer allocated until you destroy every bitmap and image source that references it.

Dispatching Input and Editing

You dispatch input to a View by allocating an event handle, firing it, and destroying the handle immediately afterward.

Input C Calls Guide
Mouse and scroll ulCreateMouseEvent(), ulViewFireMouseEvent(), ulDestroyMouseEvent(), and the matching ulCreateScrollEvent() / ulViewFireScrollEvent() / ulDestroyScrollEvent() Mouse and Scroll Input
Keys ulCreateKeyEvent(), ulViewFireKeyEvent(), ulDestroyKeyEvent() Keyboard Input
Gamepads ulSetGamepadDetails() plus the ulCreateGamepad*Event(), ulFireGamepad*Event(), and ulDestroyGamepad*Event() functions Gamepad Input
Text editing ulViewGetEditor() and the ulEditor*() functions Text Editing and the Clipboard, Input Method Editors

Keyboard Input

When creating a key event with ulCreateKeyEvent(), pass Windows virtual-key codes for virtual_key_code. The C headers do not define key-code constants.

Gamepad Input

Gamepad events are dispatched to the ULRenderer rather than an individual View.

Call ulSetGamepadDetails() on the renderer to describe each controller before firing connection, axis, or button events. For details on controller slots and event dispatching, see Gamepad Input.

Text Editing and Input Methods

Keep these details in mind when using the editor and input methods:

🚧 Avoid Replacing AppCore Editing Callbacks

In AppCore, each panel's View already registers editing callbacks to manage the window's input method. Calling ulViewSetChangeEditableStateCallback(), ulViewSetUpdateCompositionCallback(), or ulViewSetDiscardCompositionCallback() replaces those callbacks and breaks input method support for that View.