docs

Handling View Events

Listen for page load events and handle window requests from a View.

On this page

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
NetworkListener Request filtering, blocking, and security Downloads and Network Control
EditorListener Editable state changes and input method composition Keyboard Focus and Editable State, Input Method Editors

Attaching Listeners

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

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

C++
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.

👍 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, DOM Triggers and Navigation, and 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.

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

C++
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:

C++
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:

C++
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).

Here is how to request and display an inspector View:

C++
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). If these files are missing, the inspector View displays an error page.