You can attach listeners to a View to track page loading and handle requests from the page.

Load events tell you when a page finishes loading and when the DOM is ready so you can safely query the document or run scripts. You can also handle requests from the page to change the window title, set the mouse cursor, or open a new window.

## View Listeners

A View accepts several listener interfaces to report page activity and window requests.

| Listener | Purpose | Guide |
| :--- | :--- | :--- |
| `LoadListener` | Page loading progress, DOM readiness, and settling | This page |
| `ViewListener` | Requests from the page, including title, cursor, tooltips, new windows, closing, and developer tools | This page |
| `DownloadListener` | File downloads initiated by the page | [Downloads and Network Control](/docs/2.0/downloads-and-network-control) |
| `NetworkListener` | Request filtering, blocking, and security | [Downloads and Network Control](/docs/2.0/downloads-and-network-control) |
| `EditorListener` | Editable state changes and input method composition | [Keyboard Focus and Editable State](/docs/2.0/keyboard-focus-and-editable-state), [Input Method Editors](/docs/2.0/input-method-editors) |

## Attaching Listeners

To receive events from a View, implement the listener interfaces and pass a pointer to each setter on `View`:

```cpp
#include <Ultralight/Ultralight.h>
#include <vector>

using namespace ultralight;

RefPtr<Renderer> renderer;
RefPtr<View> view;

///
/// One class can implement both interfaces. Override only the callbacks you
/// need (the rest do nothing by default).
///
class MyApp : public LoadListener, public ViewListener {
 public:
  void OnDOMReady(View* caller, uint64_t frame_id, bool is_main_frame,
                  const String& url) override;

  void OnChangeTitle(View* caller, const String& title) override;

  RefPtr<View> OnCreateChildView(View* caller, const String& opener_url,
                                 const String& target_url, bool is_popup,
                                 const IntRect& popup_rect) override;

  void OnRequestClose(View* caller) override;

  RefPtr<View> OnCreateInspectorView(View* caller, bool is_local,
                                     const String& inspected_url) override;

  void OpenInspector();

 private:
  std::vector<RefPtr<View>> child_views_;
  RefPtr<View> inspector_view_;
};

MyApp app;

void AttachListeners() {
  ///
  /// Hand the View a pointer to our listener for both interfaces.
  ///
  view->set_load_listener(&app);
  view->set_view_listener(&app);
}
```

A View stores a pointer to the listener without taking ownership— the listener must stay alive for as long as the View uses it. Pass `nullptr` to the setter to remove the listener before destroying it.

A new View starts with no listeners, including child Views opened by the page. Attach listeners before loading content so you don't miss the first load's events.

> 🚧 Callback Threading and Re-entry
>
> Callbacks run on the Renderer's thread, usually during `Renderer::Update()` (handled automatically by `App::Create()`). Some callbacks fire inside one of your calls before it returns (such as `OnBeginLoading()` inside `View::LoadURL()`), so your callbacks must be ready for re-entry. Do not block inside a callback.

## Handling Load Events

You implement `LoadListener` to monitor the loading lifecycle of pages in a View.

### Event Order

A page load fires events in this order:

1. `OnBeginLoading()`— The load begins for the frame that started it, usually the main frame.
2. `OnWindowObjectReady()`— The JavaScript `window` object is ready for a frame, before its scripts run.
3. `OnDOMReady()`— The document for a frame is parsed and ready to query.
4. `OnFailLoading()`— The load failed for the frame that started it, firing only on failure.
5. `OnFinishLoading()`— The load ended for the frame that started it, whether it succeeded or failed.
6. `OnPageSettled()`— Network and layout activity have gone quiet.

### OnDOMReady

Use `OnDOMReady()` to inspect elements, read values, or call JavaScript functions once a document is parsed:

```cpp
void MyApp::OnDOMReady(View* caller, uint64_t frame_id, bool is_main_frame,
                       const String& url) {
  ///
  /// Ignore DOMReady events from child frames.
  ///
  if (!is_main_frame)
    return;

  ///
  /// The main document has loaded, so we can work with the DOM here.
  ///
}
```

The page's own `DOMContentLoaded` event has already fired by the time this callback runs. This event also fires when JavaScript is disabled.

You can access the page's JavaScript context here to evaluate scripts or call functions, as explained in [Calling into the Page](/docs/2.0/calling-into-the-page).

> 👍 Choosing Between Load Callbacks
>
> Use `OnDOMReady()` for per-page work on the DOM or page functions— if you're unsure which callback to use, choose `OnDOMReady()`. Use `OnWindowObjectReady()` only to set up JavaScript globals that page scripts need from their first line. Neither callback is needed for a `js::API`, a `dom::Triggers` set, or a data-binding context— attach them to the View once before loading, and they're already present on every page allowed by their origin rules (by default, local pages) when these events fire ([About JavaScript Interop](/docs/2.0/about-javascript-interop), [DOM Triggers and Navigation](/docs/2.0/page-wiring-and-navigation), and [About Data Bindings](/docs/2.0/about-data-bindings)).

### OnWindowObjectReady

`OnWindowObjectReady()` fires when a frame's JavaScript `window` object is created, before that frame's own scripts run and before `OnDOMReady()`.

Any `js::API` attached to the View is already available on the `window` object when this callback fires— you don't need to add it here.

This callback fires only when JavaScript is enabled. On a page with no scripts, it may not fire until you first run JavaScript on the page (such as a script call during `OnDOMReady()`), firing inside that call.

### Pages with Iframes

A page with `<iframe>` elements contains multiple documents, each running in its own frame.

The `frame_id` parameter identifies each frame, while `is_main_frame` marks the top-level document. Both `OnWindowObjectReady()` and `OnDOMReady()` fire once for every frame on the page— check `is_main_frame` to ignore child frames when you only need to interact with the main document.

The `OnBeginLoading()`, `OnFailLoading()`, and `OnFinishLoading()` callbacks fire once per load for the frame that started it (usually the main frame). You can check whether the main frame is currently loading by calling `View::is_loading()`.

### Handling Load Failures

When a load fails, `OnFailLoading()` fires immediately before `OnFinishLoading()`. It provides a description, an error domain, and an error code.

HTTP error responses count as failures. These use the domain `"HTTPErrorDomain"` with the HTTP status code as the error code. Canceled loads are not reported.

Because `OnFinishLoading()` fires whenever loading ends, regardless of outcome, you should listen to `OnFailLoading()` to detect errors.

### History Changes

The `OnUpdateHistory()` callback fires whenever the main frame's back-forward list changes, including new page navigations and calls to `View::GoBack()` or `View::GoForward()`.

You can use this callback to update the enabled state of navigation buttons by checking `View::CanGoBack()` and `View::CanGoForward()`.

## Waiting for Pages to Settle

You can wait for a page to settle before capturing it to an image or showing a remote page in your UI.

A page has settled once network and layout activity stay idle for a short time after the main frame's `onload` event fires.

> 🚧 Always Use a Timeout
>
> Pages with constant network or layout activity (such as ads, animated layouts, or streaming video) may never settle. Always pair the wait with a timeout so you don't wait indefinitely.

### Listening or Polling

You can receive the `LoadListener::OnPageSettled()` callback when the page settles, or poll `View::is_settled()` directly.

`View::is_settled()` flips to `true` when the settled event fires, and resets to `false` whenever a new navigation begins.

### Adjusting Settle Delay

`Config::page_settle_delay` configures the additional idle time in seconds that the engine waits before declaring a page settled. The default is 0.1 seconds. Increasing this value gives late network requests or layout updates more time to finish before the event fires.

### Run Loop Requirements

The engine can only detect when a page settles if you call `Renderer::Update()`, `Renderer::RefreshDisplay()`, and `Renderer::Render()` on the run loop. For details on these calls, see [Updating and Rendering](/docs/2.0/updating-and-rendering).

Applications created with `App::Create()` run these loop methods automatically.

## Responding to the Page

You implement `ViewListener` to respond when the page requests changes in the application interface.

| Callback | Action |
| :--- | :--- |
| `OnChangeTitle()` | Update the host window title to match the page. |
| `OnChangeURL()` | Update the address bar when the main frame navigates to a new page (hash changes and `history.pushState()` calls don't fire this). |
| `OnChangeTooltip()` | Draw tooltip text near the cursor and hide it when the text is empty (neither the library nor AppCore displays tooltips). |
| `OnChangeCursor()` | Update the OS or engine cursor to the requested `Cursor` (AppCore windows handle this automatically). |
| `OnAddConsoleMessage()` | Log or display messages from the page console. See [Logging and Console Messages](/docs/2.0/logging-and-console-messages). |
| `OnCreateChildView()` | Create and return a `View` when the page requests a new window. |
| `OnRequestClose()` | Close the host window displaying the `View`. |
| `OnCreateInspectorView()` | Create and return a `View` to display the developer tools. |

For simple requests like title changes, you forward the string directly to the host window:

```cpp
void MyApp::OnChangeTitle(View* caller, const String& title) {
  // Pseudo-code, set your window's title here.
  SetWindowTitle(title.utf8().data());
}
```

### Opening New Windows

When a page links to a new window with `target="_blank"` or calls `window.open()`, the library calls `OnCreateChildView()`.

To allow the window, create a View with `Renderer::CreateView()`, display it, and return it. The library automatically loads the target URL into the new View. Returning `nullptr` blocks the window from opening.

A child View starts without listeners. Attach listeners to the new View so you can track its lifecycle and handle close requests.

Here is how to create and configure a child View when the page requests a new window:

```cpp
RefPtr<View> MyApp::OnCreateChildView(View* caller, const String& opener_url,
                                      const String& target_url, bool is_popup,
                                      const IntRect& popup_rect) {
  ///
  /// Match the renderer and device scale of the View that asked.
  ///
  ViewConfig config;
  config.is_accelerated = caller->is_accelerated();
  config.initial_device_scale = caller->device_scale();
  RefPtr<View> child = renderer->CreateView(800, 600, config, nullptr);

  ///
  /// A new View has no listeners; attach ours to hear its close request.
  ///
  child->set_view_listener(this);

  child_views_.push_back(child);

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

  return child;
}
```

> 🚧 Retain Child Views
>
> The library does not keep a reference to the View returned by `OnCreateChildView()`. You must hold a reference for as long as the window is open. Dropping the final reference destroys the View.

### Handling Close Requests

The `OnRequestClose()` callback fires when a script calls `window.close()` on a window that script opened, or on a View with only one page in its history. In any other situation, the call is ignored and the page logs a console warning.

The library does not close windows or destroy views automatically. You must hide the window and release its `RefPtr<View>` reference, which is safe to do directly inside the callback.

To close the view, remove it from the display and release its reference:

```cpp
void MyApp::OnRequestClose(View* caller) {
  // Pseudo-code, close the window that shows this View here.
  HideChildView(caller);

  ///
  /// Drop our reference; the View is destroyed with its last one.
  ///
  std::erase_if(child_views_, [caller](const RefPtr<View>& child) {
    return child.get() == caller;
  });
}
```

### Opening Developer Tools

Calling `View::CreateLocalInspectorView()` opens the Web Inspector for a View. In response, the library calls `OnCreateInspectorView()` to request a View to display the developer tools.

You return a newly created View sized for your UI, or `nullptr` to cancel. The library loads the inspector into that View, which you display and route input to like any other View.

The library retains the inspector View internally and calls `OnCreateInspectorView()` only while no inspector is active for that View. You should keep a reference to the returned View to show and hide it— releasing the reference won't close the inspector, and you can't open another one for that View.

This callback also runs with `is_local` set to `false` when a remote inspector connects (see [Optimizing Performance](/docs/2.0/optimizing-performance)).

Here is how to request and display an inspector View:

```cpp
void MyApp::OpenInspector() {
  ///
  /// The inspector View is created only once, so show the one we kept.
  ///
  if (inspector_view_) {
    ShowChildView(inspector_view_);
    return;
  }

  ///
  /// The library answers by calling OnCreateInspectorView().
  ///
  view->CreateLocalInspectorView();
}

RefPtr<View> MyApp::OnCreateInspectorView(View* caller, bool is_local,
                                          const String& inspected_url) {
  ViewConfig config;
  config.is_accelerated = caller->is_accelerated();
  config.initial_device_scale = caller->device_scale();
  inspector_view_ = renderer->CreateView(1024, 600, config, nullptr);

  // Pseudo-code, display the inspector View in your UI here.
  ShowChildView(inspector_view_);

  return inspector_view_;
}
```

#### Inspector Assets

The inspector is a web application located in the SDK's **inspector** directory. When loaded, it requests `file:///inspector/Main.html` through your configured `FileSystem`.

You must copy the **inspector** folder into the root directory served by your `FileSystem` (see [Getting the SDK](/docs/2.0/getting-the-sdk)). If these files are missing, the inspector View displays an error page.
