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 (defaultfalse):invert_x,:invert_y— an axis runs the other way (defaultfalse)
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
@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
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.
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}
@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}