lib/scrollable/utility/position_cap.ex

defmodule FloUI.Scrollable.PositionCap do
  alias __MODULE__

  @moduledoc """
  Module for applying limits to a position.
  """

  @typedoc """
  A vector 2 in the form of {x, y}
  """
  @type v2 :: Scenic.Scrollable.v2()

  @typedoc """
  Data structure representing a minimum, or maximum cap which values will be compared against.
  The cap can be either a `t:v2/0` or a `t:Scenic.Scrollable.Direction.t/0`.
  By using a `t:Scenic.Scrollable.Direction/0` it is possible to cap a position only for either its x, or its y value.
  """
  @type cap :: v2 | {:horizontal, number} | {:vertical, number}

  @typedoc """
  The settings with which to initialize a `t:Scenic.Scrollable.PositionCap.t`.
  Both min and max caps are optional, and can be further limited to only the x, or y axes by passing in a `t:Scenic.Scrollable.Direction/0` rather than a `t:v2/0`.
  """
  @type settings :: %{
          optional(:max) => cap,
          optional(:min) => cap
        }

  @typedoc """
  A struct representing a position cap. Positions in the form of a `t:v2/0` can be compared against, and increased or reduced to the capped values by using the `cap/2` function.
  """
  @type t :: %PositionCap{
          max: cap,
          min: cap
        }

  defstruct max: {0, 0},
            min: {0, 0}

  @doc """
  Initializes a `t:Scenic.Scrollable.PositionCap.t/0` according to the provided `t:Scenic.Scrollable.PositionCap.settings/0`.
  """
  @spec init(settings) :: t
  def init(settings) do
    # TODO add validation in order to prevent a max value that is smaller than the min value
    # In the current code, the max value will take precedence in such case
    %PositionCap{
      max: settings[:max],
      min: settings[:min]
    }
  end

  @doc """
  Compare the upper and lower limits set in the `t:Scenic.Scrollable.PositionCap.t/0` against the `t:v2/0` provided, and adjusts the `t:v2/0` according to those limits.
  """
  @spec cap(t, v2) :: v2
  def cap(%{min: min, max: max}, coordinate) do
    coordinate
    |> floor(min)
    |> ceil(max)
  end

  @spec floor(v2, cap) :: v2
  defp floor({x, y}, {:horizontal, min_x}), do: {max(x, min_x), y}

  defp floor({x, y}, {:vertical, min_y}), do: {x, max(y, min_y)}

  defp floor({x, y}, {min_x, min_y}), do: {max(x, min_x), max(y, min_y)}

  @spec ceil(v2, cap) :: v2
  defp ceil({x, y}, {:horizontal, max_x}), do: {min(x, max_x), y}

  defp ceil({x, y}, {:vertical, max_y}), do: {x, min(y, max_y)}

  defp ceil({x, y}, {max_x, max_y}), do: {min(x, max_x), min(y, max_y)}
end