Ultralight uses a small set of foundational types across almost every C++ class and callback signature. Learning how these types manage memory, handle text, and store layout bounds lets you work with the renderer without guessing about lifetimes or ownership.

The table below summarizes the core types you will encounter throughout the API:

| Type | Description |
| --- | --- |
| `RefPtr` / `WeakPtr` | Reference-counting smart pointers that manage and observe object lifetimes. |
| `String` | Text container that stores strings in null-terminated UTF-8 format. |
| `Buffer` | Reference-counted container for raw byte data. |
| `Rect` / `IntRect` | Floating-point and integer rectangles defined by their edge coordinates. |

## `RefPtr`

Ultralight shares core objects (such as `Renderer`, `View`, `Bitmap`, and `Buffer`) between your application and the engine. Each object stores its reference count internally— `RefPtr<T>` acts as a smart pointer that holds one reference to keep it alive.

### Managing Object Lifetime

Factory functions return a new instance wrapped in a `RefPtr`. Copying the pointer increments the reference count, while resetting it, assigning `nullptr`, or letting it exit scope decrements the count. Testing the pointer in a conditional checks whether it holds a non-null object.

Creating, copying, and resetting a `RefPtr` controls the underlying object's lifetime:

```cpp
RefPtr<Bitmap> bitmap =
    Bitmap::Create(100, 100, BitmapFormat::BGRA8_UNORM_SRGB);
RefPtr<Bitmap> other = bitmap;  // two references to one Bitmap

bitmap.reset();                 // one reference left
other = nullptr;                // none left: the Bitmap is destroyed
```

> 🚧 Do Not Delete Objects Directly
>
> Never call `delete` on a library object. Let the enclosing `RefPtr` reset or fall out of scope so the object deallocates itself when its reference count drops to zero.

### Passing Raw Pointers

Many functions throughout the library accept or return a raw pointer `T*`. These pointers represent borrowed references that do not modify the object's reference count.

#### Borrowing an Instance

When a function expects a raw pointer, call `get()` on your `RefPtr` to pass the underlying object without altering its reference count.

This helper extracts a raw pointer to pass to a function that borrows the `Bitmap`:

```cpp
void Paint(Bitmap* bitmap);

void Draw(const RefPtr<Bitmap>& bitmap) {
  if (bitmap)
    Paint(bitmap.get());
}
```

#### Retaining a Callback Pointer

Callbacks often supply a raw pointer (such as `View* caller`) that is valid only for the duration of the callback. Because reference counts live inside the object itself, assigning this borrowed pointer to a `RefPtr` safely increments the count and keeps the object alive after the callback returns.

Assigning a callback's raw pointer to a member `RefPtr` preserves the `View`:

```cpp
class PageTracker : public LoadListener {
 public:
  void OnDOMReady(View* caller, uint64_t frame_id, bool is_main_frame,
                  const String& url) override {
    ready_view_ = caller;  // adds a reference: the View stays alive
  }

 private:
  RefPtr<View> ready_view_;
};
```

### Observing with `WeakPtr`

`WeakPtr<T>` observes a reference-counted object without keeping it alive. In Ultralight, `WeakPtr` supports `Bitmap` instances only, and using it with other types produces a compile error.

Calling `Lock()` returns a strong `RefPtr` that keeps the object alive while held, or `nullptr` if the object has already been destroyed. Avoid checking `Expired()` because another thread could destroy the object immediately after the check— test the return value of `Lock()` instead.

You can observe a `Bitmap` with a `WeakPtr` and lock it before drawing:

```cpp
WeakPtr<Bitmap> weak(bitmap);

// Later:
if (RefPtr<Bitmap> strong = weak.Lock())
  Paint(strong.get());
```

## `String`

Ultralight uses its own `String` class across its C++ interface rather than `std::string` because standard library layouts differ across compilers and platforms. The JavaScript and DOM bridge APIs remain the exception, accepting `std::string` directly.

### Creating and Copying Strings

`String` stores text in null-terminated UTF-8 format. It implicitly constructs from string literals, concatenates text with `+` and `+=`, and creates an independent copy of the character data when copied.

Constructing, appending to, and copying a `String` uses familiar syntax:

```cpp
String greeting = "Howdy!";
greeting += " Welcome back.";
String copy = greeting;  // an independent copy of the text
```

### Accessing UTF-8 Bytes

Call `utf8().data()` to get a pointer to the null-terminated UTF-8 byte array. The `utf8().length()` method returns the buffer size in bytes rather than the number of Unicode characters.

You can inspect the underlying UTF-8 buffer and its byte length directly:

```cpp
const char* text = greeting.utf8().data();  // null-terminated UTF-8
size_t bytes = greeting.utf8().length();    // bytes, not characters
```

### Converting to Other Encodings

When interfacing with platform APIs that require wide characters (such as Windows system functions), call `utf16()` or `utf32()`. These methods convert the text and return an independent copy as a `String16` or `String32`.

Calling `utf16()` produces a UTF-16 copy for wide-character platform APIs:

```cpp
String16 wide = greeting.utf16();  // a UTF-16 copy for a wide-char API
```

### Interoperating with `std::string`

Including the optional `<Ultralight/StringSTL.h>` header (which requires C++17) provides conversion helpers between `String` and standard library string types. The overloaded `Convert()` function converts bidirectionally based on the argument passed, while `Convert<std::string_view>()` creates a non-owning view of the internal bytes without allocating memory. If you work without this header, construct a `String` from `std::string` using `String(s.data(), s.size())`.

The conversion utilities translate between standard library strings and Ultralight strings:

```cpp
#include <Ultralight/StringSTL.h>
#include <string>
#include <string_view>
using namespace ultralight;

void Cross(const String& title) {
  std::string std_title = Convert(title);
  String back = Convert(std_title);

  std::string_view view = Convert<std::string_view>(title);  // no copy
}
```

> 🚧 String Lifetime with String Views
>
> A `std::string_view` created with `Convert<std::string_view>()` points directly into the buffer of the source `String`. That `String` must remain valid and unmodified for as long as you read the view.

## `Buffer`

`Buffer` represents a reference-counted container for raw byte data, held and passed as a `RefPtr<Buffer>`. Ultralight uses it for loading files, passing font and image assets, and transferring binary payloads.

### Wrapping Existing Memory

`Buffer::Create()` wraps existing memory without copying it. Your application retains responsibility for the allocation— the memory must stay valid and untouched until the library releases its last reference, at which point Ultralight invokes your destruction callback so you can free the memory.

Passing a destruction callback to `Buffer::Create()` frees your custom buffer when the library finishes with it:

```cpp
#include <Ultralight/Buffer.h>
#include <cstdlib>
using namespace ultralight;

RefPtr<Buffer> WrapBytes(void* bytes, size_t size) {
  return Buffer::Create(bytes, size, nullptr,
                        [](void* user_data, void* data) { free(data); });
}
```

### Copying Raw Bytes

If you cannot guarantee that your memory outlives the buffer, use `Buffer::CreateFromCopy()`. This allocates a new internal block, copies the bytes into it, and frees the allocated memory automatically when the `Buffer` instance is destroyed.

Calling `Buffer::CreateFromCopy()` creates an independent owned buffer:

```cpp
RefPtr<Buffer> copy = Buffer::CreateFromCopy(bytes, size);
```

## `Rect` and `IntRect`

Ultralight defines two rectangle types for layout, painting, and hit testing: `Rect` uses floating-point coordinates, while `IntRect` uses integers. For instance, `Surface::dirty_bounds()` returns an `IntRect` for pixel-aligned drawing areas, whereas `Window::window_control_bounds()` returns a `Rect` for subpixel UI boundaries.

### Defining Rectangle Edges

Both rectangle types store four boundary coordinates: `left`, `top`, `right`, and `bottom`, rather than an origin and dimensions. When defining a `Rect` from an origin and size, call `Rect::FromXYWH()`, which calculates the right and bottom bounds for you.

The helper constructor initializes a `Rect` from position and dimensions:

```cpp
Rect button = Rect::FromXYWH(10, 10, 120, 32);
// button.left = 10, button.top = 10, button.right = 130, button.bottom = 42
```

### Measuring and Hit Testing

Helper methods calculate dimensions and perform spatial queries. You can retrieve `width()` and `height()`, check whether a rectangle contains a point or another rectangle using `Contains()`, or test for overlap with `Intersects()`.

Member methods query dimensions and test point containment:

```cpp
float width = button.width();               // 120
bool hit = button.Contains(Point(40, 20));  // true
```

### Checking Bounds and Area

Checking whether a rectangle has content involves two distinct methods. `IsEmpty()` returns `true` only when all four boundary coordinates equal zero, whereas `IsValid()` returns `true` only when both `width()` and `height()` are strictly positive.

Evaluating a degenerate rectangle highlights the difference between emptiness and validity:

```cpp
Rect line = Rect::FromXYWH(50, 50, 0, 32);
line.IsEmpty();  // false: the edges aren't all zero
line.IsValid();  // false: it has no area
```

> 🚧 Test Visible Area with IsValid
>
> A rectangle with zero width or height is not necessarily empty. Because its coordinate offsets are non-zero, `IsEmpty()` returns `false`. Always call `IsValid()` to check whether a rectangle encloses a usable area.
