Skip to main content

priv/native/ios/mob_touch_nif.m

/* mob_touch_nif — iOS touch tier-1 plugin NIF (Objective-C).
 *
 * Streams the user's raw touches WITHOUT consuming them: a passive
 * UIGestureRecognizer is added to the key window with cancelsTouchesInView=NO
 * and it never advances past .possible, so it observes every touch
 * (began/moved/ended/cancelled) while the touches still flow to the app's
 * views. Coordinates are window points (logical, == dp); moves are throttled.
 * Mirrors the Android Window.Callback observer and its {:touch, _} contract.
 *
 * Compiled as ObjC (-fobjc-arc) by the plugin C-NIF path (manifest lang:
 * :objc). Registered as the Erlang module mob_touch_nif via ERL_NIF_INIT.
 */
#import <UIKit/UIGestureRecognizerSubclass.h>
#import <UIKit/UIKit.h>
#include <erl_nif.h>

enum { PHASE_DOWN = 0, PHASE_MOVE = 1, PHASE_UP = 2, PHASE_CANCEL = 3 };

static ErlNifPid g_pid;
static BOOL g_active = NO;
static double g_throttle_ms = 16;
static double g_last_move_ms = 0;
static int g_next_pointer = 0;
static NSMapTable *g_pointers = nil; // UITouch* (weak) -> NSNumber* id

static double now_ms(void) { return CACurrentMediaTime() * 1000.0; }

// ── {:touch, %{phase, x, y, pointer, timestamp}} delivery ──────────────────
static void send_touch(int phase, double x, double y, int pointer) {
  if (!g_active)
    return;
  ErlNifEnv *e = enif_alloc_env();
  const char *p = phase == PHASE_DOWN   ? "down"
                  : phase == PHASE_MOVE ? "move"
                  : phase == PHASE_UP   ? "up"
                                        : "cancel";
  ERL_NIF_TERM keys[5] = {enif_make_atom(e, "phase"), enif_make_atom(e, "x"),
                          enif_make_atom(e, "y"), enif_make_atom(e, "pointer"),
                          enif_make_atom(e, "timestamp")};
  ERL_NIF_TERM vals[5] = {enif_make_atom(e, p), enif_make_double(e, x),
                          enif_make_double(e, y), enif_make_int(e, pointer),
                          enif_make_int64(e, (long long)now_ms())};
  ERL_NIF_TERM map;
  enif_make_map_from_arrays(e, keys, vals, 5, &map);
  ERL_NIF_TERM msg = enif_make_tuple2(e, enif_make_atom(e, "touch"), map);
  ErlNifPid pid = g_pid;
  enif_send(NULL, &pid, e, msg);
  enif_free_env(e);
}

static int pointer_id(UITouch *t, BOOL assign) {
  NSNumber *existing = [g_pointers objectForKey:t];
  if (existing)
    return existing.intValue;
  if (!assign)
    return 0;
  int id = g_next_pointer++;
  [g_pointers setObject:@(id) forKey:t];
  return id;
}

// ── Passive observer recognizer (never recognizes; doesn't consume) ────────
@interface MobTouchObserver : UIGestureRecognizer
@end

@implementation MobTouchObserver
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  for (UITouch *t in touches) {
    CGPoint p = [t locationInView:self.view];
    send_touch(PHASE_DOWN, p.x, p.y, pointer_id(t, YES));
  }
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  double t_ms = now_ms();
  if (g_throttle_ms > 0 && t_ms - g_last_move_ms < g_throttle_ms)
    return;
  g_last_move_ms = t_ms;
  for (UITouch *t in touches) {
    CGPoint p = [t locationInView:self.view];
    send_touch(PHASE_MOVE, p.x, p.y, pointer_id(t, NO));
  }
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  for (UITouch *t in touches) {
    CGPoint p = [t locationInView:self.view];
    send_touch(PHASE_UP, p.x, p.y, pointer_id(t, NO));
    [g_pointers removeObjectForKey:t];
  }
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches
               withEvent:(UIEvent *)event {
  for (UITouch *t in touches) {
    CGPoint p = [t locationInView:self.view];
    send_touch(PHASE_CANCEL, p.x, p.y, pointer_id(t, NO));
    [g_pointers removeObjectForKey:t];
  }
}
@end

static MobTouchObserver *g_recognizer = nil;

static UIWindow *key_window(void) {
  for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
    if ([scene isKindOfClass:UIWindowScene.class]) {
      for (UIWindow *w in ((UIWindowScene *)scene).windows) {
        if (w.isKeyWindow)
          return w;
      }
    }
  }
  return UIApplication.sharedApplication.windows.firstObject;
}

// ── NIFs ────────────────────────────────────────────────────────────────────
static ERL_NIF_TERM nif_touch_start(ErlNifEnv *env, int argc,
                                    const ERL_NIF_TERM argv[]) {
  (void)argc;
  int throttle = 16;
  enif_get_int(env, argv[0], &throttle);
  enif_self(env, &g_pid);
  g_throttle_ms = throttle;
  dispatch_async(dispatch_get_main_queue(), ^{
    if (g_recognizer)
      return; // already streaming
    UIWindow *win = key_window();
    if (!win)
      return;
    g_pointers = [NSMapTable weakToStrongObjectsMapTable];
    g_next_pointer = 0;
    MobTouchObserver *r = [[MobTouchObserver alloc] init];
    r.cancelsTouchesInView = NO;
    r.delaysTouchesBegan = NO;
    r.delaysTouchesEnded = NO;
    [win addGestureRecognizer:r];
    g_recognizer = r;
    g_active = YES;
  });
  return enif_make_atom(env, "ok");
}

static ERL_NIF_TERM nif_touch_stop(ErlNifEnv *env, int argc,
                                   const ERL_NIF_TERM argv[]) {
  (void)argc;
  (void)argv;
  g_active = NO;
  dispatch_async(dispatch_get_main_queue(), ^{
    if (g_recognizer) {
      [g_recognizer.view removeGestureRecognizer:g_recognizer];
      g_recognizer = nil;
    }
    g_pointers = nil;
  });
  return enif_make_atom(env, "ok");
}

static ErlNifFunc nif_funcs[] = {
    {"touch_start", 1, nif_touch_start, 0},
    {"touch_stop", 0, nif_touch_stop, 0},
};

ERL_NIF_INIT(mob_touch_nif, nif_funcs, NULL, NULL, NULL, NULL)