Skip to main content

lib/mob_touch.ex

defmodule MobTouch do
  @moduledoc """
  Stream the user's raw screen touches to a screen — a Mob plugin.

  Unlike per-widget `on_tap` (which only fires for a tapped button), this
  observes **every touch on the app's surface** and reports its coordinates, so
  you can build drawing, custom gestures, heatmaps, joysticks, and the like.

  It **observes without consuming**: the touches still reach the app's normal UI,
  so buttons and scrolling keep working while you stream. No runtime permission.

  Updates arrive in the screen's `handle_info/2`:

      handle_info({:touch, %{phase: phase, x: x, y: y, pointer: pointer,
                             timestamp: ms}}, socket)

    * `phase` — `:down` (finger lands), `:move`, `:up` (finger lifts), or
      `:cancel` (the OS took the gesture, e.g. a system swipe).
    * `x`, `y` — **dp** (logical pixels), origin top-left of the app window;
      the same coordinate space the layout uses.
    * `pointer` — a stable id per finger across a gesture, so multi-touch is
      distinguishable. Single-finger use can ignore it.
    * `timestamp` — unix milliseconds.

  iOS: a passive `UIGestureRecognizer` on the key window. Android: a
  `Window.Callback` that observes `dispatchTouchEvent` and forwards it on.

  Call `stop/1` when you no longer need the stream — the observer stays
  installed (and keeps the screen's mailbox busy on every touch) until you do.
  """

  @doc """
  Start streaming touches to the calling screen.

  Options:
    * `:throttle_ms` — the minimum interval between `:move` deliveries (default
      `16`, ≈ 60 Hz). `:down`/`:up`/`:cancel` are never throttled. Raise it to
      cut mailbox traffic for coarse gestures; set `0` for every raw move.
  """
  @spec start(Mob.Socket.t(), keyword()) :: Mob.Socket.t()
  def start(socket, opts \\ []) do
    throttle_ms = Keyword.get(opts, :throttle_ms, 16)
    :mob_touch_nif.touch_start(throttle_ms)
    socket
  end

  @doc "Stop streaming touches and remove the observer."
  @spec stop(Mob.Socket.t()) :: Mob.Socket.t()
  def stop(socket) do
    :mob_touch_nif.touch_stop()
    socket
  end
end