# `Drafter.ScrollMath`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/scroll_math.ex#L1)

Scroll offset and viewport calculations for list-like widgets.

## Examples

    iex> Drafter.ScrollMath.clamp(50, 10, 4)
    6

    iex> Drafter.ScrollMath.ensure_visible(0, 7, 5)
    3

    iex> Drafter.ScrollMath.end_anchored_slice([1, 2, 3, 4, 5], 0, 3)
    {2, [3, 4, 5]}

# `clamp`

```elixir
@spec clamp(integer(), non_neg_integer(), pos_integer()) :: non_neg_integer()
```

Clamps `offset` so it never exceeds `max(0, content_size - viewport_size)`.

Negative offsets clamp to `0`, and content that fits the viewport pins the offset
at `0`.

## Examples

    iex> Drafter.ScrollMath.clamp(50, 10, 4)
    6

    iex> Drafter.ScrollMath.clamp(-3, 10, 4)
    0

    iex> Drafter.ScrollMath.clamp(2, 3, 10)
    0

# `end_anchored_slice`

```elixir
@spec end_anchored_slice(list() | tuple(), non_neg_integer(), pos_integer()) ::
  {non_neg_integer(), list()}
```

Returns `{start_index, slice}` for the visible portion of `data`.

Anchors the viewport to the *end* of the data and scrolls left from there,
matching the chart/timeline convention where new data arrives at the right.

`scroll_offset` counts elements back from the end, so `0` shows the newest
`viewport_size` elements. `data` may be a list or a tuple; the slice is always a
list. When the offset walks past the start, the slice shortens rather than wrapping.

## Examples

    iex> Drafter.ScrollMath.end_anchored_slice([1, 2, 3, 4, 5], 0, 3)
    {2, [3, 4, 5]}

    iex> Drafter.ScrollMath.end_anchored_slice([1, 2, 3, 4, 5], 2, 3)
    {0, [1, 2, 3]}

    iex> Drafter.ScrollMath.end_anchored_slice([1, 2, 3, 4, 5], 4, 3)
    {0, [1]}

    iex> Drafter.ScrollMath.end_anchored_slice({1, 2, 3, 4, 5}, 1, 2)
    {2, [3, 4]}

# `ensure_visible`

```elixir
@spec ensure_visible(non_neg_integer(), non_neg_integer(), pos_integer()) ::
  non_neg_integer()
```

Returns the adjusted scroll offset that keeps `target_index` visible within
a viewport of `viewport_size` rows starting at the current `scroll_offset`.

Scrolls the minimum distance: the target is placed at the top when it is above the
viewport, at the bottom when it is below, and the offset is returned unchanged when
it is already visible.

## Examples

    iex> Drafter.ScrollMath.ensure_visible(0, 7, 5)
    3

    iex> Drafter.ScrollMath.ensure_visible(10, 4, 5)
    4

    iex> Drafter.ScrollMath.ensure_visible(3, 5, 5)
    3

# `from_ratio`

```elixir
@spec from_ratio(float(), non_neg_integer(), pos_integer()) :: non_neg_integer()
```

Converts a drag ratio (0.0–1.0) to a clamped scroll offset.

The ratio is scaled by `max(0, content_size - viewport_size)` and rounded, then
clamped into that same range, so ratios outside `0.0..1.0` are safe.

## Examples

    iex> Drafter.ScrollMath.from_ratio(0.0, 100, 10)
    0

    iex> Drafter.ScrollMath.from_ratio(0.5, 100, 10)
    45

    iex> Drafter.ScrollMath.from_ratio(1.0, 100, 10)
    90

    iex> Drafter.ScrollMath.from_ratio(2.0, 100, 10)
    90

# `page`

```elixir
@spec page(integer(), integer(), non_neg_integer(), pos_integer()) ::
  non_neg_integer()
```

Move a bottom-anchored offset by whole viewports.

## Examples

    iex> Drafter.ScrollMath.page(0, 1, 100, 10)
    10

    iex> Drafter.ScrollMath.page(30, -2, 100, 10)
    10

# `scroll_by`

```elixir
@spec scroll_by(integer(), integer(), non_neg_integer(), pos_integer()) ::
  non_neg_integer()
```

Move a bottom-anchored offset by `delta` rows, clamped to the content.

A positive `delta` scrolls back into history, a negative one towards the newest
row.

## Examples

    iex> Drafter.ScrollMath.scroll_by(0, 3, 100, 10)
    3

    iex> Drafter.ScrollMath.scroll_by(3, -5, 100, 10)
    0

    iex> Drafter.ScrollMath.scroll_by(0, 999, 100, 10)
    90

# `to_oldest`

```elixir
@spec to_oldest(non_neg_integer(), pos_integer()) :: non_neg_integer()
```

The offset showing the oldest content.

## Examples

    iex> Drafter.ScrollMath.to_oldest(100, 10)
    90

    iex> Drafter.ScrollMath.to_oldest(4, 10)
    0

# `visible_range`

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

The window a bottom-anchored viewport shows, as `{first_index, count}`.

`offset` counts rows back from the bottom, so `0` is the newest `viewport_size`
rows. Content shorter than the viewport gives the whole of it. This is the form
a widget rendering virtual rows wants: it never materialises the rows outside
the window.

## Examples

    iex> Drafter.ScrollMath.visible_range(0, 100, 10)
    {90, 10}

    iex> Drafter.ScrollMath.visible_range(5, 100, 10)
    {85, 10}

    iex> Drafter.ScrollMath.visible_range(0, 4, 10)
    {0, 4}

    iex> Drafter.ScrollMath.visible_range(999, 100, 10)
    {0, 10}

---

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