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]}
Summary
Functions
Clamps offset so it never exceeds max(0, content_size - viewport_size).
Returns {start_index, slice} for the visible portion of data.
Returns the adjusted scroll offset that keeps target_index visible within
a viewport of viewport_size rows starting at the current scroll_offset.
Converts a drag ratio (0.0–1.0) to a clamped scroll offset.
Move a bottom-anchored offset by whole viewports.
Move a bottom-anchored offset by delta rows, clamped to the content.
The offset showing the oldest content.
The window a bottom-anchored viewport shows, as {first_index, count}.
Functions
@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
@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]}
@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
@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
@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
@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
@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
@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}