# `Alaja.Buffer`
[🔗](https://github.com/Lorenzo-SF/alaja/blob/2.1.0/lib/alaja/buffer.ex#L1)

2D grid of cells for rendering terminal content.

Uses a flat tuple for O(1) access per cell and minimal GC pressure.
All operations are pure — they return a new buffer without mutating
the original.

## Fields

* `width` — visible width in columns.
* `height` — visible height in rows.
* `cells` — flat tuple indexed by `y * width + x`.

## Usage

    iex> buffer = Alaja.Buffer.new(80, 24)
    iex> buffer = Alaja.Buffer.put(buffer, 10, 5, "X", {255, 0, 0})
    iex> Alaja.Buffer.get(buffer, 10, 5)

# `cell`

```elixir
@type cell() :: Alaja.Cell.t()
```

# `coordinates`

```elixir
@type coordinates() :: {non_neg_integer(), non_neg_integer()}
```

# `t`

```elixir
@type t() :: %Alaja.Buffer{
  cells: tuple(),
  height: non_neg_integer(),
  offset_x: term(),
  offset_y: term(),
  width: non_neg_integer()
}
```

# `clear`

```elixir
@spec clear(t()) :: t()
```

Clears all cells in the buffer.

## Parameters

- `buffer` - The buffer to clear

## Returns

- Buffer with all cells cleared

## Examples

    iex> buffer = Buffer.new(10, 10) |> Buffer.put(5, 5, "X", {255, 0, 0})
    iex> cleared = Buffer.clear(buffer)
    iex> Buffer.get(cleared, 5, 5).char
    " "

# `create_empty_buffer`

```elixir
@spec create_empty_buffer() :: t()
```

Creates an empty buffer (width 0, height 0).

# `crop`

```elixir
@spec crop(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer()
) :: t()
```

Crops a buffer to a sub-region. Out-of-range bounds are clamped.
Useful for slicing a layout into a window.

## Examples

    buf = Alaja.Buffer.new(10, 5)
    Alaja.Buffer.crop(buf, 2, 1, 5, 3)  # 5 cols x 3 rows starting at (2,1)

# `fill`

```elixir
@spec fill(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  String.t(),
  Alaja.Cell.color(),
  Alaja.Cell.color()
) :: t()
```

Fills a rectangular region with a cell.

## Parameters

- `buffer` - The buffer to fill
- `x1` - Starting x coordinate
- `y1` - Starting y coordinate
- `x2` - Ending x coordinate
- `y2` - Ending y coordinate
- `char` - Character to fill with
- `fg` - Foreground color
- `bg` - Background color

## Returns

- Updated buffer

# `fill_with_opts`

```elixir
@spec fill_with_opts(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  keyword()
) :: t()
```

Fill with keyword options (alternative API).

# `get`

```elixir
@spec get(t(), non_neg_integer(), non_neg_integer()) :: Alaja.Cell.t()
```

Gets the cell at the specified coordinates.

## Parameters

- `buffer` - The buffer to read from
- `x` - The x coordinate (0-based)
- `y` - The y coordinate (0-based)

## Returns

- The cell at the coordinates, or an empty cell if out of bounds

## Examples

    iex> buffer = Buffer.new(10, 10) |> Buffer.put(5, 5, "A", {255, 0, 0})
    iex> Buffer.get(buffer, 5, 5).char
    "A"

    iex> buffer = Buffer.new(10, 10)
    iex> Buffer.get(buffer, 100, 100).char
    " "

# `get_cell`

```elixir
@spec get_cell(t(), non_neg_integer(), non_neg_integer()) :: Alaja.Cell.t()
```

Gets the cell at the specified coordinates (alias for get/3).

# `hstack`

```elixir
@spec hstack([t()], non_neg_integer()) :: t()
```

Stacks buffers horizontally (left to right) with an optional gap
(in columns) between them. Resulting buffer has `sum(widths) +
gap * (n - 1)` columns and `max(heights)` rows. Buffers are
top-aligned; shorter buffers get padded with empty rows at the
bottom.

## Examples

    a = Alaja.Buffer.new(3, 1) |> Alaja.Buffer.put(0, 0, "A")
    b = Alaja.Buffer.new(3, 1) |> Alaja.Buffer.put(0, 0, "B")
    Alaja.Buffer.hstack([a, b], 1)
    # 7 cols x 1 row: 'A   B   '

# `merge`

```elixir
@spec merge(t(), t()) :: t()
```

Merges two buffers. Cells from buffer2 override cells from buffer1.

Useful for Z-index layering where higher Z buffers are merged on top.

## Parameters

- `buffer1` - Base buffer
- `buffer2` - Overlay buffer

## Returns

- Merged buffer

# `merge_matrix`

```elixir
@spec merge_matrix(t(), non_neg_integer(), non_neg_integer(), [[Alaja.Cell.t()]]) ::
  t()
```

Merges a matrix of cells into the buffer at specified position.

## Parameters

- `buffer` - The buffer to merge into
- `x_start` - Starting x coordinate
- `y_start` - Starting y coordinate
- `matrix` - 2D list of cells to merge

## Returns

- Updated buffer

# `new`

```elixir
@spec new(non_neg_integer(), non_neg_integer()) :: t()
```

Creates a new buffer with the specified dimensions.

Uses a flat tuple for O(1) access.

## Parameters

- `width` - The width of the buffer in cells
- `height` - The height of the buffer in cells

## Returns

- A new buffer struct

## Examples

    iex> Buffer.new(80, 24)
    %Buffer{width: 80, height: 24, cells: {...}}

# `overlay`

```elixir
@spec overlay(t(), t(), non_neg_integer(), non_neg_integer()) :: t()
```

Overlays `src` onto `dest` at offset `(x, y)`. Cells outside the
destination bounds are clipped. Empty cells in `src` (char `" "`,
no fg, no bg, no effects) are skipped so they don't paint over
content in `dest`.

## Examples

    dest = Alaja.Buffer.new(10, 1)
    src = Alaja.Buffer.new(3, 1) |> Alaja.Buffer.put(0, 0, "X", {255, 0, 0})
    Alaja.Buffer.overlay(dest, src, 2, 0)

# `pad`

```elixir
@spec pad(t(), non_neg_integer(), non_neg_integer()) :: t()
```

Pads a buffer to a target size. Negative padding is clamped to 0.
Content stays at the top-left.

## Examples

    Alaja.Buffer.pad(buf, 10, 5)  # buf becomes 10 cols x 5 rows, padded right/bottom

# `positioned?`

```elixir
@spec positioned?(t()) :: boolean()
```

Returns `true` if the buffer has a non-zero offset set via `with_offset/3`.

Useful when composing layouts: a positioned buffer overlays differently
than an unpositioned one.

# `put`

```elixir
@spec put(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  String.t(),
  Alaja.Cell.color(),
  Alaja.Cell.color()
) ::
  t()
```

Puts a cell at the specified coordinates.

## Parameters

- `buffer` - The buffer to modify
- `x` - The x coordinate (0-based)
- `y` - The y coordinate (0-based)
- `char` - The character to place
- `fg` - Optional foreground color as RGB tuple
- `bg` - Optional background color as RGB tuple

## Returns

- Updated buffer

## Examples

    iex> buffer = Buffer.new(10, 10)
    iex> buffer = Buffer.put(buffer, 5, 5, "X", {255, 0, 0}, {0, 0, 0})
    iex> Buffer.get(buffer, 5, 5).char
    "X"

# `range`

```elixir
@spec range(t()) :: [{non_neg_integer(), non_neg_integer()}]
```

Returns all coordinates in the buffer as a list of {x, y} tuples.

# `to_iodata`

```elixir
@spec to_iodata(t()) :: iodata()
```

Converts a buffer to iodata by iterating cells row-by-row and emitting
ANSI escapes via `Alaja.Cell.to_ansi/1`. Empty cells are coalesced into
a single space; row ends get a `
`.

## Examples

    iex> buffer = Alaja.Buffer.new(5, 1) |> Alaja.Buffer.put(0, 0, "H", {255, 0, 0})
    iex> Alaja.Buffer.to_iodata(buffer) |> IO.iodata_to_binary()
    "\e[38;2;255;0;0mH\e[0m    "

# `update_cell`

```elixir
@spec update_cell(t(), non_neg_integer(), non_neg_integer(), Alaja.Cell.t()) :: t()
```

Updates a cell at the specified coordinates with a pre-existing cell struct.

# `update_cell`

```elixir
@spec update_cell(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  String.t(),
  Alaja.Cell.color(),
  Alaja.Cell.color()
) :: t()
```

Updates a cell at the specified coordinates with character and optional colors.

# `valid_coord?`

```elixir
@spec valid_coord?(t(), non_neg_integer(), non_neg_integer()) :: boolean()
```

Checks if the coordinates are within buffer bounds.

## Parameters

- `buffer` - The buffer to check
- `x` - The x coordinate
- `y` - The y coordinate

## Returns

- `true` if coordinates are valid
- `false` otherwise

# `vstack`

```elixir
@spec vstack([t()], non_neg_integer()) :: t()
```

Stacks buffers vertically (top to bottom) with an optional gap (in
rows) between them. Resulting buffer has `sum(heights) + gap * (n - 1)`
rows and `max(widths)` columns. Buffers are left-aligned; narrower
buffers get padded with empty columns on the right.

## Examples

    a = Alaja.Buffer.new(1, 2) |> Alaja.Buffer.put(0, 0, "A")
    b = Alaja.Buffer.new(1, 2) |> Alaja.Buffer.put(0, 0, "B")
    Alaja.Buffer.vstack([a, b], 1)

# `with_offset`

```elixir
@spec with_offset(t(), non_neg_integer(), non_neg_integer()) :: t()
```

Attaches a logical offset `(x, y)` to a buffer without copying cells. The
buffer can later be rendered at this offset via
`Alaja.Printer.print_buffer/2` or composed with `Alaja.Buffer.overlay/4`.

Returns a new buffer struct with `offset_x` and `offset_y` set. Cells
are NOT moved — this is purely metadata for layout composition.

## Examples

    buf = Alaja.Components.Table.render_buffer(headers: ["A"], rows: [["1"]])
    positioned = Alaja.Buffer.with_offset(buf, 10, 5)
    positioned.offset_x  # => 10
    positioned.offset_y  # => 5

# `write`

```elixir
@spec write(t(), non_neg_integer(), non_neg_integer(), String.t()) :: t()
```

Writes a character to the buffer (alias for put/5-6).

# `write`

```elixir
@spec write(t(), non_neg_integer(), non_neg_integer(), String.t(), Alaja.Cell.color()) ::
  t()
@spec write(t(), non_neg_integer(), non_neg_integer(), String.t(), keyword()) :: t()
```

Writes a string with foreground color only (background transparent).

# `write`

```elixir
@spec write(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  String.t(),
  Alaja.Cell.color(),
  Alaja.Cell.color()
) :: t()
```

Writes a character with colors to the buffer (alias for put/5-6).

# `write_string`

```elixir
@spec write_string(t(), non_neg_integer(), non_neg_integer(), String.t()) :: t()
```

Writes a string (without colour parsing) to the buffer at the given
position. Each grapheme is written individually left-to-right.

Characters beyond the buffer width are silently skipped.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
