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:
- Callbacks belong to the handle you set them on. Each callback receives that handle as its
callerargument. - Destroying a handle disables only its callbacks. Calling
ulDestroyView()on a handle disables only the callbacks attached to that handle. - A View delivers each callback group to one handle at a time. The five groups are load events, general View requests, downloads, network requests, and text editing. Setting a callback through a second handle (eg, one from
ulPanelGetView()) moves that entire group to the new handle— earlier callbacks in that group stop firing on the first handle. - Configure all of a View's callbacks through one handle. Always set every callback for a View on the same handle so earlier callbacks in that group continue to fire.
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:
#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:
- Download buffers are borrowed. The receive-data callback delivers chunks in a
ULBufferhandle valid only for that call. Read its data withulBufferGetData()andulBufferGetSize(), and never callulDestroyBuffer(). - Network request callbacks require Pro or higher. Register a callback with
ulViewSetNetworkRequestCallback()to inspect or block traffic. The callback receives the request URL as aULString— returnfalseto block the request, ortrueto allow it.
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:
#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:
#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:
- Editor handles are borrowed. Call
ulViewGetEditor()to inspect or modify active text selection and input method state. The returnedULEditorhandle is borrowed and never NULL— it remains valid until you destroy theULViewhandle, and you do not destroy it yourself. - Zero-initialize composition segments. When styling input method composition clauses with
ulEditorSetComposition(), zero-initialize eachULCompositionSegmentstruct. A zero-initializedULColorrepresents an unset color.
🚧 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(), orulViewSetDiscardCompositionCallback()replaces those callbacks and breaks input method support for that View.