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](/docs/2.0/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.

```cpp
#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()`.

<!-- tabs:start -->
```cpp
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");
```
```c
#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");
}
```
<!-- tabs:end -->

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

<!-- tabs:start -->
```cpp
panel_ = window_->AddPanel();  // fills the window and tracks its size
panel_->view()->LoadURL("file:///page.html");
```
```c
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);
}
```
<!-- tabs:end -->

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.

<!-- tabs:start -->
```cpp
// In the constructor:
window_->set_listener(this);

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

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

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

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.

<!-- tabs:start -->
```cpp
int main() {
  MyApp app;
  app.Run();

  return 0;
}
```
```c
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;
}
```
<!-- tabs:end -->

`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](/docs/2.0/building-a-desktop-app) walks through expanding this window into a full desktop application.
