# `MingaEditor.Viewport`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga_editor/viewport.ex#L1)

Viewport logic for scrolling the visible region of a buffer.

The viewport defines which lines and columns are currently visible
in the terminal. When the cursor moves outside the viewport, it
scrolls to keep the cursor visible.

# `t`

```elixir
@type t() :: %MingaEditor.Viewport{
  cols: pos_integer(),
  left: non_neg_integer(),
  reserved: non_neg_integer(),
  rows: pos_integer(),
  top: non_neg_integer()
}
```

A viewport representing the visible terminal region.

* `top`      — first visible buffer line (0-indexed)
* `left`     — first visible **display column** (0-indexed, in terminal columns).
               Wide characters (CJK, emoji) occupy 2 display columns, so
               horizontal scroll advances by display columns, not grapheme counts.
* `rows`     — total rows in this viewport (including reserved rows)
* `cols`     — total columns in this viewport
* `reserved` — rows reserved for non-content elements (modeline, minibuffer).
               Defaults to `footer_rows()` (2) for the terminal-level viewport.
               Set to 0 for per-window viewports where Layout already excluded
               the modeline from the content rect.

# `bottom_on`

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

Scrolls so the cursor line is at the bottom of the viewport (`zb` in vim).

Respects scroll_margin by placing the cursor `margin` lines from the bottom.

# `center_on`

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

Centers the viewport on the given cursor line (`zz` in vim).

Returns the updated viewport.

# `content_cols`

```elixir
@spec content_cols(t(), non_neg_integer()) :: pos_integer()
```

Returns the number of columns available for buffer content after the gutter.

Subtracts `gutter_width(line_count)` from the viewport's total columns,
clamped to at least 1.

# `content_rows`

```elixir
@spec content_rows(t()) :: pos_integer()
```

Returns the number of content rows (total rows minus reserved).

# `effective_page_lines`

```elixir
@spec effective_page_lines(
  non_neg_integer(),
  pos_integer(),
  Minga.Core.Decorations.t(),
  non_neg_integer()
) :: pos_integer()
```

Computes how many buffer lines fit in `display_rows` screen rows,
accounting for decorations that consume extra display rows.

Walks forward from `cursor_line`, counting each buffer line as 1 display
row plus any virtual lines and block decorations attached to it. Stops
when the display row budget is exhausted. Returns the number of buffer
lines traversed.

When there are no decorations, this returns `display_rows` (the fast path).

# `effective_rows`

```elixir
@spec effective_rows(pos_integer(), number()) :: pos_integer()
```

Adjusts the raw row count for a line spacing multiplier.

When `line_spacing > 1.0`, each line takes more vertical space, so fewer
lines fit on screen. Returns `floor(rows / line_spacing)`, clamped to at
least 1. Returns the input unchanged when spacing is 1.0 (the TUI default).

The caller reads `Config.get(:line_spacing)` and passes it here. This keeps
Viewport a pure module with no config dependency.

# `footer_rows`

```elixir
@spec footer_rows() :: pos_integer()
```

Number of rows reserved for the footer (modeline + minibuffer).

# `gutter_width`

```elixir
@spec gutter_width(non_neg_integer()) :: pos_integer()
```

Computes the gutter width for line numbers based on total line count.

Returns `max(digits(line_count), 2) + 1` — at least 2 digits plus a
trailing space separator. For example: 1–99 lines → 3, 100–999 → 4.

# `new`

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

Creates a new viewport with the given dimensions and default reserved rows (2).

# `new`

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

Creates a new viewport with the given dimensions and explicit reserved rows.

# `scroll_line_down`

```elixir
@spec scroll_line_down(t(), non_neg_integer(), non_neg_integer()) ::
  {t(), non_neg_integer()}
```

Scrolls the viewport down by one line without moving the cursor.

The cursor line is clamped to remain visible and respect scroll_margin.
When scrolling down, the cursor is pushed away from the top edge to
maintain the margin, matching vim's scrolloff behavior for Ctrl-E.
Returns `{updated_viewport, clamped_cursor_line}`.

# `scroll_line_down`

```elixir
@spec scroll_line_down(t(), non_neg_integer(), non_neg_integer(), non_neg_integer()) ::
  {t(), non_neg_integer()}
```

Scrolls the viewport down by one line with an explicit scroll margin.

# `scroll_line_up`

```elixir
@spec scroll_line_up(t(), non_neg_integer(), non_neg_integer()) ::
  {t(), non_neg_integer()}
```

Scrolls the viewport up by one line without moving the cursor.

The cursor line is clamped to remain visible and respect scroll_margin.
When scrolling up, the cursor is pushed away from the bottom edge to
maintain the margin, matching vim's scrolloff behavior for Ctrl-Y.
Returns `{updated_viewport, clamped_cursor_line}`.

# `scroll_line_up`

```elixir
@spec scroll_line_up(t(), non_neg_integer(), non_neg_integer(), non_neg_integer()) ::
  {t(), non_neg_integer()}
```

Scrolls the viewport up by one line with an explicit scroll margin.

# `scroll_to_cursor`

```elixir
@spec scroll_to_cursor(
  t(),
  {non_neg_integer(), non_neg_integer()}
) :: t()
```

Scrolls the viewport to keep the cursor visible.

Returns a new viewport adjusted so that the cursor position `{line, col}`
is within the visible area. `col` must be a **display column** (terminal
columns, not grapheme count) — wide characters count as 2. Reserves footer
rows for the modeline and minibuffer.

# `scroll_to_cursor`

```elixir
@spec scroll_to_cursor(
  t(),
  {non_neg_integer(), non_neg_integer()},
  pid() | non_neg_integer()
) :: t()
```

Scrolls the viewport with a scroll margin.

Accepts either a buffer pid (reads `scroll_margin` from the buffer's
local options) or an explicit integer margin. The margin keeps `n`
lines visible above and below the cursor when possible. When the file
is shorter than `2 * margin + 1`, the margin shrinks to fit.

# `top_on`

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

Scrolls so the cursor line is at the top of the viewport (`zt` in vim).

Respects scroll_margin by placing the cursor `margin` lines from the top.

# `visible_range`

```elixir
@spec visible_range(t()) :: {non_neg_integer(), non_neg_integer()}
```

Returns the range of visible lines as `{first_line, last_line}` (inclusive).

---

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