> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/ui/ui-reference.md).

# UI Reference

Reference notes and patterns for CSL UI drawing.

This page collects UI rules, patterns, and examples that are helpful once you know the basics.

Read [UI Fundamentals](/ui/fundamentals.md) first.

### Core principles

1. **Y grows upward.** Screen coordinates use (0, 0) at the bottom-left.
2. **Start from screen rects.** Use `UI.get_safe_screen_rect()` or `UI.get_screen_rect()`, then derive everything from those rects.
3. **Push/pop pattern.** Use `defer` for every push to avoid leaking UI state.
4. **Mobile-first.** Avoid hover-only interactions.
5. **Text uses UTF-8, but character coverage is incomplete.** A character still needs a glyph in the selected font or its fallback fonts. Some characters, including emoji, are not supported yet.
6. **Use the player late-update stack.** Draw all player UI from that player's `ao_late_update` under `is_local_or_server()`.

### Quick start: a simple HUD button

```go
Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if this.is_local_or_server() {
            draw_my_hud(this);
        }
    }
}

draw_my_hud :: proc(player: Player) {
    rect := UI.get_safe_screen_rect()
        .bottom_right_rect()
        .grow(40, 150, 40, 150)
        .offset(-50, 50);

    bs := UI.default_button_settings();
    ts := UI.default_text_settings();

    if UI.button(rect, bs, ts, "Action").clicked {
        log_info("action from %", {player.get_username()});
    }
}
```

### Basic drawing

#### Quads and images

```go
draw_ui :: proc() {
    rect := UI.get_screen_rect().center_rect().grow(100); // 200x200
    texture := get_asset(Texture_Asset, "my_icon.png");
    UI.quad(rect, texture);

    // With color tint (RGBA 0-1)
    UI.quad(rect, texture, {1, 0, 0, 0.5});

    // Solid color using the white sprite
    UI.quad(rect, core_globals.white_sprite, {0, 0, 0, 0.75});
}
```

#### Filled quads

Pass `Quad_Params.fill` from `UI.quad_fill` to reveal only part of a quad. The available `Fill_Direction` values are `.NONE`, `.RIGHT`, `.LEFT`, `.UP`, `.DOWN`, and `.RADIAL`.

Linear fills use amounts from `0` to `1`. Radial fills start at 12 o'clock and use a signed amount: `-1..0` fills counterclockwise and `0..1` fills clockwise. Values outside the applicable range are clamped. A zero radial amount draws nothing; `-1` and `1` both draw the complete quad.

```csl
draw_cooldown :: proc(rect: Rect, icon: Texture_Asset, amount: float) {
    params: Quad_Params;
    params.fill = UI.quad_fill(clamp(amount, -1, 1), .RADIAL);
    UI.quad(rect, icon, params=params);
}
```

For a changing fill, keep a persistent `Float_Interpolation_Helper` and pass its result to `UI.quad_fill`. Clamp before calling `update`; preserving the sign is required for counterclockwise radial fills. Use `UI.push_fill_amount` and `UI.pop_fill_amount` instead when the same fill should apply to several draw calls. Always pair the push with a deferred pop.

#### Text

```go
draw_text :: proc() {
    rect := UI.get_screen_rect().center_rect().grow(200, 300, 50, 300);

    ts := UI.default_text_settings();
    ts.size = 48;
    ts.color = {1, 1, 1, 1};
    ts.halign = .CENTER;
    ts.valign = .CENTER;

    UI.text(rect, ts, "Hello, World!");

    score := 1500;
    UI.text(rect, ts, "Score: %", {score});

    pct := 67;
    UI.text(rect, ts, "% %%", {pct}); // "67 %"

    // Returns the actual rendered rect
    actual_rect := UI.text_sync(rect, ts, "Dynamic text");
}
```

### Layout: start from rects

```go
layout_example :: proc() {
    screen := UI.get_screen_rect();
    safe := UI.get_safe_screen_rect();

    center := screen.center_rect();
    top_left := screen.top_left_rect();
    bottom_right := screen.bottom_right_rect();

    button_rect := center.grow(50, 150, 50, 150);
    icon_rect := center.grow(64);
}
```

### Use cut for layout

The cut functions **must** be used for layouting when placing multiple UI elements:

```go
draw_panel :: proc() {
    rect := UI.get_safe_screen_rect().inset(20);

    header := rect.cut_top(80);
    footer := rect.cut_bottom(60);
    body := rect;

    UI.quad(header, core_globals.white_sprite, {0.1, 0.1, 0.1, 0.8});
    UI.quad(footer, core_globals.white_sprite, {0.1, 0.1, 0.1, 0.8});
    UI.quad(body, core_globals.white_sprite, {0.05, 0.05, 0.05, 0.8});
}
```

### Auto-scaling and unscaled rects

Regular rect functions take **points** and scale them by `UI.get_current_scale_factor()` (based on a 1080-point-tall reference canvas).

Use the `_unscaled` variants only when a value is already expressed in actual screen pixels, such as a dimension measured from an existing rect:

```go
layout_with_unscaled :: proc(window_rect: Rect, items: []string) {
    ts := UI.default_text_settings();
    list_item := window_rect.top_rect().grow_bottom(20);

    for item: items {
        UI.text(list_item, ts, item);

        // height() is already in screen pixels, so do not scale it again
        list_item = list_item.offset_unscaled(0, -list_item.height());
    }
}
```

### Buttons

```go
draw_buttons :: proc() {
    rect := UI.get_safe_screen_rect()
        .bottom_center_rect()
        .grow(40, 150, 40, 150)
        .offset(0, 100);

    bs := UI.default_button_settings();
    ts := UI.default_text_settings();

    bs.sprite = get_asset(Texture_Asset, "$AO/new/modal/buttons_2/button_2.png");
    bs.press_scaling = 0.35;

    result := UI.button(rect, bs, ts, "Click Me!");
    if result.clicked {
        log_info("Button was clicked!");
    }
}
```

#### Button sprite rules

`UI.default_button_settings()` already provides a complete button style. For a custom button, set `sprite` and optionally `sprite_hovered` and `sprite_pressed`. When the optional sprites are null, the base sprite is reused.

### Modals

Use `UI.begin_modal` / `UI.end_modal` for a dimmed backdrop that closes when the player taps outside the modal or presses Escape.

```go
Player :: class : Player_Base {
    settings_open: bool;
}

draw_settings_modal :: proc(player: Player) {
    if !player.settings_open return;

    UI.begin_modal(UI.get_screen_rect(), "settings_modal", ref player.settings_open);
    defer UI.end_modal();

    window := UI.get_safe_screen_rect().center_rect().grow(240, 360, 240, 360);
    UI.quad(window, core_globals.white_sprite, {0, 0, 0, 0.9});

    ts := UI.default_text_settings();
    ts.halign = .CENTER;
    ts.valign = .CENTER;
    UI.text(window, ts, "Settings");
}
```

### UI state management

Use `defer` for every push/pop pair:

```go
draw_layered_ui :: proc() {
    UI.push_screen_draw_context();
    defer UI.pop_draw_context();

    UI.push_layer(100);
    defer UI.pop_layer();

    UI.push_color_multiplier({1, 1, 1, 0.5});
    defer UI.pop_color_multiplier();

    // Draw UI here
}
```

### IDs for repeated elements

When drawing lists or repeated items, push unique IDs:

```go
draw_list :: proc(items: []string) {
    rect := UI.get_safe_screen_rect().inset(20);
    bs := UI.default_button_settings();
    ts := UI.default_text_settings();

    for i: 0..<items.count {
        UI.push_id("item_%", {i});
        defer UI.pop_id();

        item_rect := rect.cut_top(60);
        if UI.button(item_rect, bs, ts, items[i]).clicked {
            log_info("selected item %", {i});
        }
    }
}
```

### Common sizes

Some sizes that are known to look good across devices:

* Screen text points: Title 52, Body 36
* Screen rect points: Simple dialog 600x400, Standard button 210x74, Exit button 65x65
* World-space text size: 0.30 meters

### Best practices

* Use `defer` for every push/pop pair.
* Push IDs for repeated elements.
* Use unscaled functions with computed dimensions.
* Fit icon aspect ratios with `rect.fit_aspect(texture.get_aspect())`.
* Always check `is_local_or_server()` before drawing interactive UI.
