RasterExRatatui.Input.Touch (RasterExRatatui v0.4.0)

Copy Markdown View Source

Turns evdev touch events into the ExRatatui.Event.Mouse events an app expects: a tap is a "down" and an "up" on the cell under the finger, a finger moving into another cell is a "drag".

Pure and table-free: keep the struct between calls, since it holds the contact in progress. A panel reports its finger in its own axis units and its own orientation; the translator scales the axes to the panel's pixels, applies swap_xy: and invert_x:/invert_y: when the controller does not follow the panel, and asks cell_at: (normally RasterExRatatui.Raster.cell_at/2 of the surface's raster, which knows the rotation) which cell that pixel is. A finger outside the grid, in the margins, produces nothing until it enters a cell.

Only the first contact counts: multitouch slots other than 0 are ignored, so a second finger never moves the cursor. Both multitouch type B (abs_mt_slot, abs_mt_tracking_id, abs_mt_position_x/y, what the Goodix controller of the Raspberry Pi Touch Display 2 sends) and single-touch panels (abs_x/y with btn_touch) are understood.

Mouse events are emitted at the end of each evdev frame. input_event delivers one frame per message and drops the syn_report that ends it, so translate_all/2 takes the end of its list as the end of a frame; an explicit syn_report ends one too, for sources that keep it.

Options

new/1 takes:

  • :size (required) — the physical panel in pixels, {width, height}
  • :cell_at (required) — ({px, py} -> {col, row} | :outside)

  • :axes — %{x: {min, max}, y: {min, max}} as the controller reports them (RasterExRatatui.Input.Devices.touch_axes/1); default the pixel ranges, {0, width - 1} and {0, height - 1}
  • :swap_xy — the controller's x runs along the panel's height (default false)
  • :invert_x, :invert_y — an axis runs the other way (default false)

Examples

A 24×16 panel of 6×8 cells; a tap on pixel (13, 9) is cell (2, 1).

iex> alias RasterExRatatui.Input.Touch
iex> touch = Touch.new(size: {24, 16}, cell_at: fn {x, y} -> {div(x, 6), div(y, 8)} end)
iex> # The two messages input_event sends for a tap on a Goodix panel.
iex> {touch, [down]} = Touch.translate_all(touch, [
...>   {:ev_abs, :abs_mt_tracking_id, 33}, {:ev_abs, :abs_mt_position_x, 13},
...>   {:ev_abs, :abs_mt_position_y, 9}, {:ev_abs, :abs_mt_touch_major, 46},
...>   {:ev_key, :btn_touch, 1}, {:ev_abs, :abs_x, 13}, {:ev_abs, :abs_y, 9}
...> ])
iex> {_touch, [up]} = Touch.translate_all(touch, [{:ev_abs, :abs_mt_tracking_id, -1}, {:ev_key, :btn_touch, 0}])
iex> [{down.kind, down.x, down.y}, {up.kind, up.x, up.y}]
[{"down", 2, 1}, {"up", 2, 1}]

Summary

Functions

Builds the translator (see the moduledoc for the options). Raises ArgumentError without :size or :cell_at.

Folds one evdev event, {type, code, value}, and returns the mouse events it completes: only a syn_report ends a frame here. For input_event's messages, which carry no syn_report, use translate_all/2.

Folds one frame of events, as input_event delivers it, or the :disconnect it sends when the panel goes away, which ends a contact in progress with an "up".

Types

axes()

@type axes() :: %{x: {integer(), integer()}, y: {integer(), integer()}}

t()

@type t() :: %RasterExRatatui.Input.Touch{
  axes: axes(),
  cell: {integer(), integer()} | nil,
  cell_at: ({non_neg_integer(), non_neg_integer()} ->
              {integer(), integer()} | :outside),
  invert_x: boolean(),
  invert_y: boolean(),
  position: {integer() | nil, integer() | nil},
  releasing?: boolean(),
  size: {pos_integer(), pos_integer()},
  slot: integer(),
  swap_xy: boolean(),
  touching?: boolean()
}

Functions

new(opts)

@spec new(keyword()) :: t()

Builds the translator (see the moduledoc for the options). Raises ArgumentError without :size or :cell_at.

translate(t, arg2)

@spec translate(t(), {atom(), atom(), integer()}) ::
  {t(), [ExRatatui.Event.Mouse.t()]}

Folds one evdev event, {type, code, value}, and returns the mouse events it completes: only a syn_report ends a frame here. For input_event's messages, which carry no syn_report, use translate_all/2.

Examples

iex> alias RasterExRatatui.Input.Touch
iex> touch = Touch.new(size: {24, 16}, cell_at: fn {x, y} -> {div(x, 6), div(y, 8)} end)
iex> {touch, []} = Touch.translate(touch, {:ev_abs, :abs_x, 20})
iex> {touch, []} = Touch.translate(touch, {:ev_abs, :abs_y, 3})
iex> {touch, []} = Touch.translate(touch, {:ev_key, :btn_touch, 1})
iex> {_touch, [down]} = Touch.translate(touch, {:ev_syn, :syn_report, 0})
iex> {down.kind, down.button, down.x, down.y}
{"down", "left", 3, 0}

translate_all(t, events)

@spec translate_all(t(), [{atom(), atom(), integer()}] | :disconnect) ::
  {t(), [ExRatatui.Event.Mouse.t()]}

Folds one frame of events, as input_event delivers it, or the :disconnect it sends when the panel goes away, which ends a contact in progress with an "up".

The end of the list ends the frame. A list that carries its own syn_reports is split at them instead, and nothing more is emitted after the last one.

Examples

A finger landing on one cell and sliding to the next; the second finger (slot 1) changes nothing.

iex> alias RasterExRatatui.Input.Touch
iex> touch = Touch.new(size: {24, 16}, cell_at: fn {x, y} -> {div(x, 6), div(y, 8)} end)
iex> {touch, [down]} = Touch.translate_all(touch, [{:ev_abs, :abs_mt_tracking_id, 1}, {:ev_abs, :abs_mt_position_x, 2}, {:ev_abs, :abs_mt_position_y, 2}])
iex> {touch, []} = Touch.translate_all(touch, [{:ev_abs, :abs_mt_position_x, 4}])
iex> {touch, []} = Touch.translate_all(touch, [{:ev_abs, :abs_mt_slot, 1}, {:ev_abs, :abs_mt_tracking_id, 2}, {:ev_abs, :abs_mt_position_x, 20}, {:ev_abs, :abs_mt_position_y, 12}])
iex> {touch, [drag]} = Touch.translate_all(touch, [{:ev_abs, :abs_mt_slot, 0}, {:ev_abs, :abs_mt_position_x, 9}])
iex> {_touch, [up]} = Touch.translate_all(touch, :disconnect)
iex> [{down.kind, down.x, down.y}, {drag.kind, drag.x, drag.y}, {up.kind, up.x, up.y}]
[{"down", 0, 0}, {"drag", 1, 0}, {"up", 1, 0}]

A finger in the margin is ignored until it enters a cell.

iex> alias RasterExRatatui.Input.Touch
iex> outside = fn {x, y} -> if x < 12, do: {div(x, 6), div(y, 8)}, else: :outside end
iex> touch = Touch.new(size: {24, 16}, cell_at: outside)
iex> {touch, []} = Touch.translate_all(touch, [{:ev_key, :btn_touch, 1}, {:ev_abs, :abs_x, 20}, {:ev_abs, :abs_y, 0}])
iex> {_touch, [down]} = Touch.translate_all(touch, [{:ev_abs, :abs_x, 7}])
iex> {down.kind, down.x, down.y}
{"down", 1, 0}