docs

App Lifecycle and Settings

Configure the App singleton, run the main event loop, and schedule tasks across threads.

On this page

An App object manages the lifetime of a desktop application. It coordinates window creation, platform integrations, and the main event loop so you can run a complete desktop interface from one central place.

You configure the application at startup, start its loop, and schedule background work onto the main thread.

Creating the App

Call App::Create() at startup to initialize the library and create the application singleton:

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

using namespace ultralight;

int main() {
  Settings settings;
  settings.developer_name = "Acme";
  settings.app_name = "Rocket Planner";
  RefPtr<App> app = App::Create(settings, Config());

  // Create your windows here.

  app->Run();
  return 0;
}

Create only one App instance during an application's lifetime.

Both arguments to App::Create() are optional. Pass Settings to configure AppCore behavior, and pass Config to set renderer options (see Creating the Renderer).

Call App::instance() to access the application singleton from anywhere in your app.

Call App::renderer() to access the underlying Renderer created and owned by the app— use it for renderer-level calls like managing sessions or creating Views directly.

Platform Defaults

App::Create() fills any platform interfaces that remain unset with default implementations for the host OS.

You can set custom platform handlers before calling App::Create()— any interface already assigned is preserved (see Setting Up the Platform).

Slot Default
Logger Writes ultralight.log to the application's diagnostics folder.
File system Read-only file loader rooted at Settings::file_system_path.
Font loader Loads fonts installed on the host OS.
Clipboard Integrates with the OS clipboard.
GPU driver Uses the platform's native GPU backend (or a surface factory under the CPU renderer).
Audio output Uses the OS audio device (available only in SDK editions with audio and video support).

Configuring Settings

Pass a Settings struct to App::Create() to configure file paths, renderer choices, and display properties:

Setting Default Description
developer_name, app_name "MyCompany", "MyApp" Names the per-app folders used for logs, profiler traces, and session storage. Set these before shipping (see Shipping Your App).
file_system_path "./assets/" Root folder for file:/// URLs, relative to the executable folder on Windows and Linux, or YourApp.app/Contents/Resources/ on macOS.
force_cpu_renderer false Forces CPU rendering even when a supported GPU is present.
device_scale_override 0.0 Sets a fixed display scale for every window when positive, useful for testing different monitor DPIs.
app_icon null Sets the macOS Dock icon and default window icon on Windows and Linux, using a 32-bit BGRA bitmap with straight alpha.
enable_profiler false Enables the built-in profiler to write a Perfetto trace file (requires Pro edition or higher— see Profiling and Tracing).

By default, auto_font_profile, auto_font_families, and match_native_editing_behavior are all true, so font styling, standard font families, and text editing conventions follow the host OS (see Native Look and Feel).

Running the Main Loop

Starting and Stopping the Loop

Call App::Run() to start the main event loop. The call blocks until App::Quit() is called.

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

🚧 Modal loops on macOS

On macOS, calling App::Quit() while a modal loop is active (such as a native alert dialog or an interactive window drag) is absorbed by that inner loop. Call App::Quit() again once the modal interaction completes.

Handling Per-Frame Updates

Implement AppListener::OnUpdate() to execute native logic on every iteration of the main loop. The method fires immediately before the renderer updates.

Register the listener with App::set_listener(). You retain ownership of the listener instance.

Running an External Loop

When an application provides its own outer loop, call App::RunOnce() on each tick instead of App::Run():

C++
while (app->RunOnce()) {
  StepSimulation();
}

App::RunOnce() pumps pending OS events, updates the renderer, and repaints windows that are due. It accepts an optional max_wait_seconds idle timeout (defaulting to 0.0 to return immediately), and returns false once App::Quit() is called.

📘 Running without AppCore

When managing windows and the run loop directly without AppCore, you must call Renderer::Update(), Renderer::RefreshDisplay(), and Renderer::Render() on each frame. See Updating and Rendering.

Scheduling Tasks and Timers

Ultralight requires all API calls to occur on the main thread. You can use tasks and timers to dispatch work onto the main thread safely from any thread.

Posting Tasks

Call App::PostTask() to execute a callable once on the main thread during a future loop update:

C++
///
/// Load the save file on a worker thread, then hand the result to the
/// main thread (capture by value: the lambda outlives this scope).
///
std::thread([] {
  std::string save = ReadSaveFile();
  App::instance()->PostTask([save] { ShowSave(save); });
}).detach();

Tasks execute in the order they are posted. When posting a lambda from another thread, capture variables by value because the callable outlives the enclosing scope.

Call App::PostDelayedTask() to run a callable on the main thread after a specified delay in milliseconds.

Scheduling Repeating Timers

Call App::SetInterval() to schedule a repeating callback on the main thread:

C++
class Clock {
 public:
  void Start() {
    ///
    /// Tick once a second until the handle is destroyed or canceled.
    ///
    tick_ = App::instance()->SetInterval(1000, [this] { UpdateClock(); });
  }

  void Stop() { tick_.Cancel(); }

 private:
  void UpdateClock();
  TimerHandle tick_;
};

App::SetInterval() returns a TimerHandle that owns the timer. The timer stops when the handle is destroyed or when you call TimerHandle::Cancel().

Store the handle in a persistent location (such as a class member). Discarding the handle cancels the timer immediately.

Detecting Idle Time

The application enters an idle state when user input stops and main-thread CPU usage remains low for a sustained period.

Implement AppListener::OnIdle() to run background maintenance when the application is idle:

C++
///
/// Do deferred work while the user is away. Keep each slice short so
/// the app feels instant when they come back.
///
void OnIdle(double utilization) override {
  if (utilization < 0.1)
    SaveDrafts();
}

void SaveDrafts() {
  // Write unsaved work to disk.
}

The utilization parameter reports recent CPU usage between 0.0 and 1.0. Callbacks pause as soon as new input arrives or CPU usage rises above idle_utilization_threshold.

Three properties in Settings configure how the application detects idle time:

Setting Default Description
idle_threshold 0.5 s Time without user input before idle detection begins.
sustained_idle_time 2.0 s Continuous time the application must stay idle (no input past idle_threshold and CPU usage below idle_utilization_threshold) before the first AppListener::OnIdle() fires, and the repeat interval while idle continues. By default, the first callback occurs at least 2.5 seconds after the last user input.
idle_utilization_threshold 0.5 Main-thread CPU usage (from 0.0 to 1.0) below which the application counts as idle.

Headless and Batch Workloads

Windows created with WindowFlags::Hidden receive no OS paint events and do not paint by default. Set Settings::headless_paint_fps to a positive frame rate to paint hidden windows at a fixed rate independent of monitor refresh rates— useful for automated testing and offscreen capture.

Set Settings::full_speed_loop to true to run the main loop continuously without sleeping, letting resource loading, scripts, and layout advance as fast as the CPU allows.

🚧 Loop thread saturation

Enabling full_speed_loop causes the loop thread to busy-wait, fully occupying one CPU core. Use this setting only for batch or offscreen rendering workloads, never for interactive desktop applications.