The single process the native layer delivers interaction events to.
Native knows two things and neither of them is a screen: the registered name
:mob_screen (used by enif_whereis_pid for the back gesture, alert actions
and the launch-notification fallback, on both platforms) and whatever pid was
stored in a tap handle by register_tap/1. This module takes over the second.
The envelope
nif_register_tap stores an arbitrary term as the handle's tag and echoes it
back verbatim — mob_send_tap sends {:tap, tag}, mob_send_event sends
{event, tag}. The tag is copied with enif_make_copy, so it can be any
shape, including a nested tuple.
So instead of registering {screen_pid, tag}, Mob.Renderer registers
{listener_pid, {:mob_route, screen_pid, tag}}Native then delivers {:tap, {:mob_route, screen_pid, tag}} here, and the
listener forwards {:tap, tag} to the screen. Native remains ignorant that
screens exist, and no .m, .zig or generator-template change is
required to move the inbound path off a single hard-wired screen process.
Native has two message shapes for handle-addressed events, and the listener has to unwrap both:
{event, tag}—mob_send_tap,mob_send_event,mob_send_scrolled_past. Covers:tap,:focus,:blur,:submit,:dismiss,:selectand the other payload-free events.{event, tag, payload}—mob_send_change,mob_send_compose,mob_send_swipe_with_direction,mob_send_scroll,mob_send_drag,mob_send_pinch,mob_send_rotate,mob_send_pointer_move. This is everything carrying a value: text-field and toggle and slideron_change, tab selection, and every gesture stream.
Both are unwrapped on the event atom rather than one clause per event, so a new event kind needs no change here — but a new arity would. Anything else is logged rather than silently discarded, because an unmodelled shape is invisible otherwise: the widget simply stops working.
Why a hop at all
Today there is one screen process, so carrying its pid through the envelope
and forwarding is, on its own, a hop that buys nothing. What it buys is that
the ~35 register_tap call sites in Mob.Renderer stop naming a screen
process directly. When MOB-112 makes screens processes and MOB-113 adds the
router, the change is confined to handler/1 and handle_info/2 here rather
than spread across every interactive prop in the renderer.
The escape hatch
A high-frequency stream — drag, scroll, mob_touch at display rate — pays one
extra hop and one extra copy per event. Registering the screen pid directly
bypasses this module entirely and still works, because that is exactly what
the renderer did before:
nif.register_tap({screen_pid, tag}) # direct, no listenerNothing bypasses it today. The hop has not been measured, and adding an exception before there is a number to point at would be guessing.
Summary
Functions
Returns a specification to start this module under a supervisor.
Start the listener if it is not already running.
Wrap a register_tap/1 target so native delivers the event here instead of
straight to the screen.
Whether the listener is running.
Start the listener. Named, so there is exactly one.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec ensure_started() :: :ok
Start the listener if it is not already running.
Unlinked, for the same reason Mob.Sender.ensure_started/0 is: the caller is
usually a screen, and a screen crash must not take down the process every
screen's events arrive through.
Wrap a register_tap/1 target so native delivers the event here instead of
straight to the screen.
Accepts either shape the renderer uses — a bare pid, or {pid, tag} — and
returns the term to hand to register_tap/1.
Returns the target unchanged when the listener is not running, so events go directly to the screen exactly as they did before this module existed. That is the fallback for any boot path that does not start a listener, and it is what keeps the renderer's own tests working without one.
@spec running?() :: boolean()
Whether the listener is running.
@spec start_link(keyword()) :: GenServer.on_start()
Start the listener. Named, so there is exactly one.