docs

Writing Your First App

Load and display HTML in a native OS window.

On this page

This tutorial walks you through the 02-basic-app sample which uses Ultralight and the AppCore module to display HTML in a native desktop window.

1. Build the Sample

First, build and run the sample so you have an idea of the end-result.

👍 Build the samples

If you haven't built the SDK samples yet, see Trying the Samples.

Then, open the source code for the 02-basic-app sample from the SDK's samples folder in your favorite text editor or IDE so you can follow along.

2. Program Structure

The sample defines a single class, MyApp, to manage the application and its window.

C++
#include <AppCore/App.h>
#include <AppCore/Window.h>

using namespace ultralight;

class MyApp : public WindowListener {
  RefPtr<App> app_;
  RefPtr<Window> window_;
  RefPtr<Panel> panel_;

 public:
  MyApp() {
    // The code from the next steps goes here.
  }

  void Run() { app_->Run(); }
};

MyApp inherits from WindowListener and stores the App, Window, and Panel in RefPtr smart pointers. The constructor configures the window and loads the page, while Run() starts the event loop.

3. Create the App and Window

At the beginning, notice that the sample code calls App::Create().

app_ = App::Create();

// 900 by 600 logical pixels (the monitor's DPI sets the real size).
window_ = Window::Create(app_->main_monitor(), 900, 600, false,
                         WindowFlags::Titled | WindowFlags::Hidden);
window_->SetTitle("Ultralight Sample 2 - Basic App");
#include <AppCore/CAPI.h>

static ULApp app = NULL;
static ULWindow window = NULL;

void CreateAppAndWindow(void) {
  ///
  /// Create our main App instance (it manages the lifetime of the
  /// application and is required to create any windows). NULL settings
  /// and config mean the defaults.
  ///
  app = ulCreateApp(NULL, NULL);

  ///
  /// Create our Window. The size (900 by 600) is in DPI-independent
  /// logical pixels-- the actual pixel size follows the monitor's DPI.
  ///
  window = ulCreateWindow(ulAppGetMainMonitor(app), 900, 600, false,
                          kWindowFlags_Titled | kWindowFlags_Hidden);

  ///
  /// Set the title of our window.
  ///
  ulWindowSetTitle(window, "Ultralight Sample 2 - Basic App");
}

App::Create() sets up the renderer and platform integration. You need this App instance to create windows and run the main event loop.

Window::Create() creates a native OS window on the main monitor, sized in DPI-independent logical pixels. Passing WindowFlags::Hidden keeps the window off screen while the page loads— step 6 displays it once the content is ready.

🚧 Only create one App

You should only create one App instance per application lifetime.

👍 Combining window flags

You can combine window flags with the bitwise OR operator (|). For example, WindowFlags::Titled | WindowFlags::Resizable creates a resizable window. To see all available flags and display options, see Windows, Monitors, and DPI.

4. Add a Panel and Load a Page

The sample code calls Window::AddPanel() without arguments to create a panel that fills the entire window.

panel_ = window_->AddPanel();  // fills the window and tracks its size
panel_->view()->LoadURL("file:///page.html");
static ULPanel panel = NULL;
static ULView view = NULL;

void AddPanelAndLoadPage(void) {
  ///
  /// Add a web-content panel that spans the entire window and tracks
  /// its size automatically (NULL options and view config mean the defaults).
  ///
  panel = ulWindowAddPanel(window, NULL, NULL);

  ///
  /// Get the panel's View. This is a new reference we own, so we destroy
  /// it at teardown (that releases only our reference, never the panel's).
  ///
  view = ulPanelGetView(panel);

  ///
  /// Load a local HTML file into our panel's View. Strings are created
  /// for the call and destroyed once it returns.
  ///
  ULString url = ulCreateString("file:///page.html");
  ulViewLoadURL(view, url);
  ulDestroyString(url);
}

Every window organizes its contents using a layout tree of panels. The window manages the panel's size, paints it, and routes input to it.

Call View::LoadURL() on the panel's underlying View to load the HTML page.

📘 Resolving file URLs

file:/// URLs resolve relative to Settings::file_system_path, which defaults to ./assets/. This folder sits beside the executable on Windows and Linux, and inside YourApp.app/Contents/Resources/ on macOS. Files placed in assets/ ship directly with the app.

5. Handle Window Close

To handle window events, register MyApp as the window's listener in the constructor.

// In the constructor:
window_->set_listener(this);

// Inherited from WindowListener:
void OnClose(ultralight::Window* window) override {
  app_->Quit();
}
///
/// Exit the application when the window is closed.
///
static void OnClose(void* user_data, ULWindow closed_window) {
  ulAppQuit(app);
}

Window::set_listener() registers the listener without taking ownership. When the user closes the window, WindowListener::OnClose() fires and calls App::Quit().

Calling App::Quit() stops the event loop so that App::Run() returns— it does not terminate the process itself.

6. Show the Window

Call Window::ShowWhenReady() at the end of the constructor to reveal the window.

// At the end of the constructor:
window_->ShowWhenReady();
///
/// Show the window once its page is ready (wait at most 0.5 seconds).
///
ulWindowShowWhenReady(window, 0.5);

Because the window was created hidden in step 3, Window::ShowWhenReady() keeps it off-screen until the page loads and settles. This ensures the first frame on screen is the finished page rather than a blank window.

By default, the call waits up to 0.5 seconds for the page to settle— if loading takes longer, the window shows anyway.

7. Run the Application

The main() function instantiates MyApp and starts the event loop.

int main() {
  MyApp app;
  app.Run();

  return 0;
}
int main(void) {
  CreateAppAndWindow();
  AddPanelAndLoadPage();

  ///
  /// Register our close callback. The last two arguments are the
  /// callback's user_data and the function that frees it-- we need neither.
  ///
  ulWindowSetCloseCallback(window, OnClose, NULL, NULL);

  ///
  /// Show the window once its page is ready.
  ///
  ulWindowShowWhenReady(window, 0.5);

  ///
  /// Run the app until the window is closed.
  ///
  ulAppRun(app);

  ///
  /// Destroy what we created, in reverse order: the View, the panel,
  /// the window, then the App.
  ///
  ulDestroyView(view);
  ulDestroyPanel(panel);
  ulDestroyWindow(window);
  ulDestroyApp(app);

  return 0;
}

App::Run() blocks until App::Quit() is called. On each pass through the loop, the application handles OS events, forwards input to panels, and updates the renderer.

Update Native App Logic

To run custom app logic on every pass through the loop, implement AppListener::OnUpdate() and attach the listener using App::set_listener().

8. Modify the Page

You can now edit assets/page.html to customize the interface.

🚧 Rebuild to see asset changes

The build system copies files from assets/ into the output folder. Edit assets/page.html and rebuild to see changes in the window.

What's Next

From here, Building a Desktop App walks through expanding this window into a full desktop application.