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:

```cpp
#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](/api/cpp/2_0_0/structultralight_1_1_settings.html) to configure AppCore behavior, and pass `Config` to set renderer options (see [Creating the Renderer](/docs/2.0/creating-the-renderer#content-configuring-the-library)).

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](/docs/2.0/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](/docs/2.0/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](/docs/2.0/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](/docs/2.0/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()`:

```cpp
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](/docs/2.0/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:

```cpp
///
/// 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:

```cpp
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:

```cpp
///
/// 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.
