# `Tela.Frame`
[🔗](https://github.com/idabmat/tela/blob/v0.1.0/lib/tela/frame.ex#L1)

A rendered frame returned by `c:Tela.view/1` and `c:Tela.Component.view/1`.

A `Frame` carries two pieces of information: the rendered UI content as a
plain string, and an optional cursor position that tells the runtime where
to place the real terminal cursor after rendering.

## Content

`content` is the full UI string for this frame, with lines separated by
`\n`. It is passed directly to `Tela.Renderer.diff/2` for line-diff
rendering, exactly as the `String.t()` return from `view/1` was previously.

## Cursor

`cursor` is either `nil` (hide the terminal cursor) or a
`{row, col, shape}` tuple:

- `row` — 0-indexed row relative to this frame's top-left.
- `col` — 0-indexed column relative to this frame's top-left.
- `shape` — `:block`, `:bar`, or `:underline`.

Cursor coordinates are **relative to the component's own top-left**. When
frames are composed with `join/2`, the accumulated row offset of each
preceding frame is added to the cursor row of the frame that holds it.

## Composition

Use `join/2` to stack frames vertically. It concatenates content strings
with a separator (default `"\n"`), adjusts the cursor row of each frame by
the cumulative line count of all preceding frames and their separators, and
takes the cursor from the first frame in the list that has a non-nil cursor.

    a = Frame.new("Line A")
    b = Frame.new("Line B", cursor: {0, 3, :block})
    Frame.join([a, b])
    # => %Frame{content: "Line A\nLine B", cursor: {1, 3, :block}}

## Usage in `view/1`

A top-level app with no interactive cursor:

    def view(model) do
      Frame.new("Count: #{model.count}\nPress q to quit.")
    end

A top-level app composing a `TextInput` component:

    def view(model) do
      header = Frame.new("What's your name?\n")
      input  = TextInput.view(model.input)
      footer = Frame.new("\n(esc to quit)")
      Frame.join([header, input, footer], separator: "")
    end

# `cursor`

```elixir
@type cursor() ::
  {row :: non_neg_integer(), col :: non_neg_integer(), shape :: cursor_shape()}
```

A cursor position within a frame. Coordinates are 0-indexed and relative
to the frame's own top-left corner.

# `cursor_shape`

```elixir
@type cursor_shape() :: :block | :bar | :underline
```

The shape of the terminal cursor.

# `t`

```elixir
@type t() :: %Tela.Frame{content: String.t(), cursor: cursor() | nil}
```

A rendered frame, optionally carrying a cursor position.

# `join`

```elixir
@spec join([t()], [{:separator, String.t()}]) :: t()
```

Stacks a list of frames vertically, producing a single composed frame.

Content strings are joined with `separator` (default `"\n"`). The cursor
is taken from the **first frame in the list that has a non-nil cursor**,
with its row adjusted by the cumulative line count of all preceding frames
and their separators.

## Options

- `separator:` — the string inserted between each frame's content
  (default `"\n"`).

## Examples

    iex> alias Tela.Frame
    iex> a = Frame.new("Line A")
    iex> b = Frame.new("Line B", cursor: {0, 2, :block})
    iex> Frame.join([a, b])
    %Frame{content: "Line A\nLine B", cursor: {1, 2, :block}}

    iex> alias Tela.Frame
    iex> frames = [Frame.new("A"), Frame.new("B"), Frame.new("C")]
    iex> Frame.join(frames)
    %Frame{content: "A\nB\nC", cursor: nil}

# `new`

```elixir
@spec new(content :: String.t()) :: t()
```

Returns a new frame with the given content and no cursor.

The terminal cursor will be hidden while this frame is displayed.

# `new`

```elixir
@spec new(content :: String.t(), [{:cursor, cursor() | nil}]) :: t()
```

Returns a new frame with the given content and cursor options.

## Options

- `cursor:` — `{row, col, shape}` tuple or `nil`. Coordinates are 0-indexed
  and relative to this frame's top-left. Shape is `:block`, `:bar`, or
  `:underline`. `nil` hides the cursor (equivalent to `new/1`).

---

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