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](/docs/2.0/creating-views) and the input and editing guides.

General conventions for memory ownership, callback user data, and threading follow [C API Conventions](/docs/2.0/c-api-conventions). To set up the renderer and manage sessions, see [Renderer in C](/docs/2.0/c-renderer).

## 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](/docs/2.0/c-api-conventions#content-keeping-a-borrowed-value)). |
| `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](/docs/2.0/your-first-game-ui). For available configuration options, see [Creating Views](/docs/2.0/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](/docs/2.0/c-api-conventions#content-what-destroying-a-handle-does).

## 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](/docs/2.0/c-api-conventions#content-using-callbacks-and-user-data).

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](/docs/2.0/handling-view-events) |
| `ViewListener` | `ulViewSetChangeTitleCallback()`, `ulViewSetCreateChildViewCallback()`, `ulViewSetRequestCloseCallback()`, and other View setters (URL, tooltip, cursor, console message, and inspector View) | [Handling View Events](/docs/2.0/handling-view-events) |
| `DownloadListener` | Six `ulViewSetDownload*Callback()` setters | [Downloads and Network Control](/docs/2.0/downloads-and-network-control) |
| `NetworkListener` | `ulViewSetNetworkRequestCallback()` | [Downloads and Network Control](/docs/2.0/downloads-and-network-control) |
| `EditorListener` | `ulViewSetChangeEditableStateCallback()`, `ulViewSetUpdateCompositionCallback()`, and `ulViewSetDiscardCompositionCallback()` | [Input Method Editors](/docs/2.0/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 `caller` argument.
- **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](/docs/2.0/handling-view-events#content-opening-new-windows).

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](/docs/2.0/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 `ULBuffer` handle valid only for that call. Read its data with `ulBufferGetData()` and `ulBufferGetSize()`, and never call `ulDestroyBuffer()`.
- **Network request callbacks require Pro or higher.** Register a callback with `ulViewSetNetworkRequestCallback()` to inspect or block traffic. The callback receives the request URL as a `ULString`— return `false` to block the request, or `true` to 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](/docs/2.0/updating-and-rendering#content-displaying-cpu-views).

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](/docs/2.0/c-platform-handlers).

### 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](/docs/2.0/c-gpu-driver).

## 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](/docs/2.0/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](/docs/2.0/mouse-and-scroll-input) |
| Keys | `ulCreateKeyEvent()`, `ulViewFireKeyEvent()`, `ulDestroyKeyEvent()` | [Keyboard Input](/docs/2.0/keyboard-input) |
| Gamepads | `ulSetGamepadDetails()` plus the `ulCreateGamepad*Event()`, `ulFireGamepad*Event()`, and `ulDestroyGamepad*Event()` functions | [Gamepad Input](/docs/2.0/gamepad-input) |
| Text editing | `ulViewGetEditor()` and the `ulEditor*()` functions | [Text Editing and the Clipboard](/docs/2.0/text-editing-and-clipboard), [Input Method Editors](/docs/2.0/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](/docs/2.0/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 returned `ULEditor` handle is borrowed and never NULL— it remains valid until you destroy the `ULView` handle, and you do not destroy it yourself.
- **Zero-initialize composition segments.** When styling input method composition clauses with `ulEditorSetComposition()`, zero-initialize each `ULCompositionSegment` struct. A zero-initialized `ULColor` represents 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()`, or `ulViewSetDiscardCompositionCallback()` replaces those callbacks and breaks input method support for that View.

