Mob.Listener (mob v0.7.39)

Copy Markdown View Source

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, :select and 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 slider on_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 listener

Nothing 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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

ensure_started()

@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.

handler(target)

@spec handler(pid() | {pid(), term()}) :: pid() | {pid(), term()}

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.

running?()

@spec running?() :: boolean()

Whether the listener is running.

start_link(opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

Start the listener. Named, so there is exactly one.