diff --git a/input/drivers/switch_input.c b/input/drivers/switch_input.c index d1a78b1593..386657056e 100644 --- a/input/drivers/switch_input.c +++ b/input/drivers/switch_input.c @@ -18,6 +18,62 @@ #define SWITCH_MAX_SCANCODE 0xfb /* see https://switchbrew.github.io/libnx/hid_8h.html */ #define MOUSE_MAX_X 1920 #define MOUSE_MAX_Y 1080 + +/* beginning of touch mouse defines and types */ +#define TOUCH_MAX_X 1280 +#define TOUCH_MAX_Y 720 +#define TOUCH_MOUSE_BUTTON_LEFT 0 +#define TOUCH_MOUSE_BUTTON_RIGHT 1 +#define NO_TOUCH -1 /* finger id setting if finger is not touching the screen */ + +enum +{ + MAX_NUM_FINGERS = 3, /* number of fingers to track per panel for touch mouse */ + MAX_TAP_TIME = 250, /* taps longer than this will not result in mouse click events */ + MAX_TAP_MOTION_DISTANCE = 10, /* max distance finger motion in Vita screen pixels to be considered a tap */ + SIMULATED_CLICK_DURATION = 50, /* time in ms how long simulated mouse clicks should be */ +}; /* track three fingers per panel */ + +typedef struct +{ + int id; /* -1: no touch */ + int time_last_down; + float last_down_x; + float last_down_y; +} Touch; + +typedef enum DraggingType +{ + DRAG_NONE = 0, + DRAG_TWO_FINGER, + DRAG_THREE_FINGER, +} DraggingType; + +typedef enum TouchEventType +{ + FINGERDOWN, + FINGERUP, + FINGERMOTION, +} TouchEventType; + +typedef struct +{ + TouchEventType type; + uint64_t timestamp; + int touchId; + int fingerId; + float x; + float y; + float dx; + float dy; +} FingerType; + +typedef struct +{ + TouchEventType type; + FingerType tfinger; +} TouchEvent; +/* end of touch mouse defines and types */ #endif #include "../input_driver.h" @@ -34,6 +90,7 @@ typedef struct switch_input bool blocked; #ifdef HAVE_LIBNX + /* pointer */ uint32_t touch_scale_x; uint32_t touch_scale_y; @@ -41,12 +98,14 @@ typedef struct switch_input uint32_t touch_half_resolution_y; bool touch_state[MULTITOUCH_LIMIT]; + bool previous_touch_state[MULTITOUCH_LIMIT]; uint32_t touch_x[MULTITOUCH_LIMIT]; uint32_t touch_y[MULTITOUCH_LIMIT]; uint32_t touch_previous_x[MULTITOUCH_LIMIT]; uint32_t touch_previous_y[MULTITOUCH_LIMIT]; bool keyboard_state[SWITCH_MAX_SCANCODE + 1]; + /* physical mouse */ int32_t mouse_x; int32_t mouse_y; int32_t mouse_x_delta; @@ -55,28 +114,53 @@ typedef struct switch_input bool mouse_button_left; bool mouse_button_right; bool mouse_button_middle; + uint64_t mouse_previous_report; + + /* touch mouse */ + bool touch_mouse_indirect; + float touch_mouse_speed_factor; + int hires_dx; /* sub-pixel touch mouse precision */ + int hires_dy; /* sub-pixel touch mouse precision */ + Touch finger[MAX_NUM_FINGERS]; /* keep track of finger status for touch mouse */ + DraggingType multi_finger_dragging; /* keep track whether we are currently drag-and-dropping */ + int32_t simulated_click_start_time[2]; /* initiation time of last simulated left or right click (zero if no click) */ #endif } switch_input_t; +#ifdef HAVE_LIBNX +/* beginning of touch mouse function declarations */ +static void handle_touch_mouse(switch_input_t *sw); +static void normalize_touch_mouse_xy(float *normalized_x, float *normalized_y, int reported_x, int reported_y); +static void process_touch_mouse_event(switch_input_t *sw, TouchEvent *event); +static void process_touch_mouse_finger_down(switch_input_t * sw, TouchEvent *event); +static void process_touch_mouse_finger_up(switch_input_t *sw, TouchEvent *event); +static void process_touch_mouse_finger_motion(switch_input_t *sw, TouchEvent *event); +static void normalized_to_screen_xy(int *screenX, int *screenY, float x, float y); +static void finish_simulated_mouse_clicks(switch_input_t *sw, uint64_t currentTime); +/* end of touch mouse function declarations */ +#endif + static void switch_input_poll(void *data) { switch_input_t *sw = (switch_input_t*) data; - - if (sw->joypad) - sw->joypad->poll(); - #ifdef HAVE_LIBNX - uint32_t touch_count = hidTouchCount(); + uint32_t touch_count; unsigned int i = 0; int keySym = 0; unsigned keyCode = 0; uint16_t mod = 0; MousePosition mouse_pos; - bool previous_touch_state[MULTITOUCH_LIMIT]; + uint64_t mouse_current_report = 0; +#endif + if (sw->joypad) + sw->joypad->poll(); + +#ifdef HAVE_LIBNX + touch_count = hidTouchCount(); for (i = 0; i < MULTITOUCH_LIMIT; i++) { - previous_touch_state[i] = sw->touch_state[i]; + sw->previous_touch_state[i] = sw->touch_state[i]; sw->touch_state[i] = touch_count > i; if (sw->touch_state[i]) @@ -88,12 +172,6 @@ static void switch_input_poll(void *data) sw->touch_previous_y[i] = sw->touch_y[i]; sw->touch_x[i] = touch_position.px; sw->touch_y[i] = touch_position.py; - // prevent jumps in mouse pointer when putting down finger - if (!previous_touch_state[i]) - { - sw->touch_previous_x[i] = sw->touch_x[i]; - sw->touch_previous_y[i] = sw->touch_y[i]; - } } } @@ -122,67 +200,59 @@ static void switch_input_poll(void *data) } } - if (hidMouseButtonsHeld() & MOUSE_LEFT) + /* update physical mouse buttons only when they change + * this allows the physical mouse and touch mouse to coexist */ + mouse_current_report = hidMouseButtonsHeld(); + if ((mouse_current_report & MOUSE_LEFT) != (sw->mouse_previous_report & MOUSE_LEFT)) { - sw->mouse_button_left = true; - } - else - { - sw->mouse_button_left = false; + if (mouse_current_report & MOUSE_LEFT) + sw->mouse_button_left = true; + else + sw->mouse_button_left = false; } - if (hidMouseButtonsHeld() & MOUSE_RIGHT) + if ((mouse_current_report & MOUSE_RIGHT) != (sw->mouse_previous_report & MOUSE_RIGHT)) { - sw->mouse_button_right = true; - } - else - { - sw->mouse_button_right = false; + if (mouse_current_report & MOUSE_RIGHT) + sw->mouse_button_right = true; + else + sw->mouse_button_right = false; } - if (hidMouseButtonsHeld() & MOUSE_MIDDLE) + if ((mouse_current_report & MOUSE_MIDDLE) != (sw->mouse_previous_report & MOUSE_MIDDLE)) { - sw->mouse_button_middle = true; - } - else - { - sw->mouse_button_middle = false; + if (mouse_current_report & MOUSE_MIDDLE) + sw->mouse_button_middle = true; + else + sw->mouse_button_middle = false; } + sw->mouse_previous_report = mouse_current_report; + /* physical mouse position */ hidMouseRead(&mouse_pos); sw->mouse_x_delta = mouse_pos.velocityX; sw->mouse_y_delta = mouse_pos.velocityY; - // allow finger to move mouse pointer like on a laptop touchpad - for (i = 0; i < MULTITOUCH_LIMIT; i++) - { - if (sw->touch_state[i]) - { - sw->mouse_x_delta += sw->touch_x[i] - sw->touch_previous_x[i]; - sw->mouse_y_delta += sw->touch_y[i] - sw->touch_previous_y[i]; - } - } - sw->mouse_x += sw->mouse_x_delta; sw->mouse_y += sw->mouse_y_delta; + + /* touch mouse events + * handle_touch_mouse will update sw->mouse_* variables + * depending on touch input gestures + * see first comment in process_touch_mouse_event() for a list of + * supported touch gestures */ + handle_touch_mouse(sw); + if (sw->mouse_x < 0) - { sw->mouse_x = 0; - } else if (sw->mouse_x > MOUSE_MAX_X) - { sw->mouse_x = MOUSE_MAX_X; - } if (sw->mouse_y < 0) - { sw->mouse_y = 0; - } else if (sw->mouse_y > MOUSE_MAX_Y) - { sw->mouse_y = MOUSE_MAX_Y; - } sw->mouse_wheel = mouse_pos.scrollVelocityY; #endif @@ -233,9 +303,7 @@ static int16_t switch_input_mouse_state(switch_input_t *sw, unsigned id, bool sc break; case RETRO_DEVICE_ID_MOUSE_X: if (screen) - { val = sw->mouse_x; - } else { val = sw->mouse_x_delta; @@ -244,9 +312,7 @@ static int16_t switch_input_mouse_state(switch_input_t *sw, unsigned id, bool sc break; case RETRO_DEVICE_ID_MOUSE_Y: if (screen) - { val = sw->mouse_y; - } else { val = sw->mouse_y_delta; @@ -279,7 +345,7 @@ static int16_t switch_input_state(void *data, unsigned port, unsigned device, unsigned idx, unsigned id) { - int16_t ret = 0; + int16_t ret = 0; switch_input_t *sw = (switch_input_t*) data; if (port > MAX_PADS-1) @@ -342,6 +408,364 @@ static int16_t switch_input_state(void *data, return 0; } +#ifdef HAVE_LIBNX +void handle_touch_mouse(switch_input_t *sw) +{ + int finger_id = 0; + uint64_t current_time = svcGetSystemTick() * 1000 / 19200000; + unsigned int i; + finish_simulated_mouse_clicks(sw, current_time); + + for (i = 0; i < MULTITOUCH_LIMIT; i++) + { + if (sw->touch_state[i]) + { + float x = 0; + float y = 0; + normalize_touch_mouse_xy(&x, &y, sw->touch_x[i], sw->touch_y[i]); + finger_id = i; + + /* Send an initial touch if finger hasn't been down */ + if (!sw->previous_touch_state[i]) + { + TouchEvent ev; + ev.type = FINGERDOWN; + ev.tfinger.timestamp = current_time; + ev.tfinger.fingerId = finger_id; + ev.tfinger.x = x; + ev.tfinger.y = y; + process_touch_mouse_event(sw, &ev); + } + else + { + /* If finger moved, send motion event instead */ + if (sw->touch_x[i] != sw->touch_previous_x[i] || + sw->touch_y[i] != sw->touch_previous_y[i]) + { + float oldx = 0; + float oldy = 0; + TouchEvent ev; + normalize_touch_mouse_xy(&oldx, &oldy, sw->touch_previous_x[i], sw->touch_previous_y[i]); + ev.type = FINGERMOTION; + ev.tfinger.timestamp = current_time; + ev.tfinger.fingerId = finger_id; + ev.tfinger.x = x; + ev.tfinger.y = y; + ev.tfinger.dx = x - oldx; + ev.tfinger.dy = y - oldy; + process_touch_mouse_event(sw, &ev); + } + } + } + + /* some fingers might have been let go */ + if (sw->previous_touch_state[i] == true && sw->touch_state[i] == false) + { + float x = 0; + float y = 0; + TouchEvent ev; + normalize_touch_mouse_xy(&x, &y, sw->touch_previous_x[i], sw->touch_previous_y[i]); + finger_id = i; + /* finger released from screen */ + ev.type = FINGERUP; + ev.tfinger.timestamp = current_time; + ev.tfinger.fingerId = finger_id; + ev.tfinger.x = x; + ev.tfinger.y = y; + process_touch_mouse_event(sw, &ev); + } + } +} + +void normalize_touch_mouse_xy(float *normalized_x, float *normalized_y, int reported_x, int reported_y) +{ + float x = 0; + float y = 0; + x = (float) reported_x / TOUCH_MAX_X; + y = (float) reported_y / TOUCH_MAX_Y; + + if (x < 0.0) + x = 0.0; + else if (x > 1.0) + x = 1.0; + + if (y < 0.0) + y = 0.0; + else if (y > 1.0) + y = 1.0; + *normalized_x = x; + *normalized_y = y; +} + +void process_touch_mouse_event(switch_input_t *sw, TouchEvent *event) +{ + /* supported touch gestures: + * pointer motion = single finger drag + * left mouse click = single finger short tap + * right mouse click = second finger short tap while first finger is still down + * left click drag and drop = dual finger drag + * right click drag and drop = triple finger drag */ + if (event->type == FINGERDOWN || event->type == FINGERUP || event->type == FINGERMOTION) + { + switch (event->type) + { + case FINGERDOWN: + process_touch_mouse_finger_down(sw, event); + break; + case FINGERUP: + process_touch_mouse_finger_up(sw, event); + break; + case FINGERMOTION: + process_touch_mouse_finger_motion(sw, event); + break; + } + } +} + +void process_touch_mouse_finger_down(switch_input_t *sw, TouchEvent *event) +{ + /* id (for multitouch) */ + int id = event->tfinger.fingerId; + unsigned int i; + + /* make sure each finger is not reported down multiple times */ + for (i = 0; i < MAX_NUM_FINGERS; i++) + { + if (sw->finger[i].id == id) + sw->finger[i].id = NO_TOUCH; + } + + /* we need the timestamps to decide later if the user performed a short tap (click) + * or a long tap (drag) + * we also need the last coordinates for each finger to keep track of dragging */ + for (i = 0; i < MAX_NUM_FINGERS; i++) + { + if (sw->finger[i].id == NO_TOUCH) + { + sw->finger[i].id = id; + sw->finger[i].time_last_down = event->tfinger.timestamp; + sw->finger[i].last_down_x = event->tfinger.x; + sw->finger[i].last_down_y = event->tfinger.y; + break; + } + } +} + +void process_touch_mouse_finger_up(switch_input_t *sw, TouchEvent *event) +{ + /* id (for multitouch) */ + int id = event->tfinger.fingerId; + int num_fingers_down; + unsigned int i; + + /* find out how many fingers were down before this event */ + num_fingers_down = 0; + for (i = 0; i < MAX_NUM_FINGERS; i++) + { + if (sw->finger[i].id >= 0) + num_fingers_down++; + } + + for (i = 0; i < MAX_NUM_FINGERS; i++) + { + if (sw->finger[i].id != id) + continue; + + sw->finger[i].id = NO_TOUCH; + if (!sw->multi_finger_dragging) + { + float xrel; + float yrel; + float max_r_squared; + int simulated_button; + + if ((event->tfinger.timestamp - sw->finger[i].time_last_down) > MAX_TAP_TIME) + continue; + + /* short (tfinger.x * TOUCH_MAX_X) - (sw->finger[i].last_down_x * TOUCH_MAX_X)); + yrel = ((event->tfinger.y * TOUCH_MAX_Y) - (sw->finger[i].last_down_y * TOUCH_MAX_Y)); + max_r_squared = (float) (MAX_TAP_MOTION_DISTANCE * MAX_TAP_MOTION_DISTANCE); + if ((xrel * xrel + yrel * yrel) >= max_r_squared) + continue; + if (num_fingers_down != 2 && num_fingers_down != 1) + continue; + simulated_button = 0; + if (num_fingers_down == 2) + { + simulated_button = TOUCH_MOUSE_BUTTON_RIGHT; + /* need to raise the button later */ + sw->simulated_click_start_time[TOUCH_MOUSE_BUTTON_RIGHT] = event->tfinger.timestamp; + } + else if (num_fingers_down == 1) + { + if (!sw->touch_mouse_indirect) + { + int x; + int y; + normalized_to_screen_xy(&x, &y, event->tfinger.x, event->tfinger.y); + sw->mouse_x_delta = x - sw->mouse_x; + sw->mouse_y_delta = y - sw->mouse_y; + sw->mouse_x = x; + sw->mouse_y = y; + } + simulated_button = TOUCH_MOUSE_BUTTON_LEFT; + /* need to raise the button later */ + sw->simulated_click_start_time[TOUCH_MOUSE_BUTTON_LEFT] = event->tfinger.timestamp; + } + /* simulate mouse button down */ + if (simulated_button == TOUCH_MOUSE_BUTTON_LEFT) + sw->mouse_button_left = true; + else if (simulated_button == TOUCH_MOUSE_BUTTON_RIGHT) + sw->mouse_button_right = true; + } + else if (num_fingers_down == 1) + { + /* when dragging, and the last finger is lifted, the drag is over + * so simulate mouse button up */ + if (sw->multi_finger_dragging == DRAG_THREE_FINGER) + sw->mouse_button_right = false; + else + sw->mouse_button_left = false; + sw->multi_finger_dragging = DRAG_NONE; + } + } +} + +void process_touch_mouse_finger_motion(switch_input_t *sw, TouchEvent *event) +{ + /* id (for multitouch) */ + int id = event->tfinger.fingerId; + unsigned int i; + unsigned int j; + int num_fingers_down; + bool update_pointer; + + /* find out how many fingers were down before this event */ + num_fingers_down = 0; + for (i = 0; i < MAX_NUM_FINGERS; i++) + { + if (sw->finger[i].id >= 0) + num_fingers_down++; + } + + if (num_fingers_down == 0) + return; + + /* If we are starting a multi-finger drag, start holding down the mouse button */ + if (num_fingers_down >= 2 && !sw->multi_finger_dragging) + { + /* only start a multi-finger drag if at least two fingers have been down long enough */ + int num_fingers_down_long = 0; + for (i = 0; i < MAX_NUM_FINGERS; i++) + { + if (sw->finger[i].id == NO_TOUCH) + continue; + if (event->tfinger.timestamp - sw->finger[i].time_last_down > MAX_TAP_TIME) + num_fingers_down_long++; + } + if (num_fingers_down_long >= 2) + { + int simulated_button = 0; + if (num_fingers_down_long == 2) + { + simulated_button = TOUCH_MOUSE_BUTTON_LEFT; + sw->multi_finger_dragging = DRAG_TWO_FINGER; + } + else + { + simulated_button = TOUCH_MOUSE_BUTTON_RIGHT; + sw->multi_finger_dragging = DRAG_THREE_FINGER; + } + + if (simulated_button == TOUCH_MOUSE_BUTTON_LEFT) + sw->mouse_button_left = true; + else if (simulated_button == TOUCH_MOUSE_BUTTON_RIGHT) + sw->mouse_button_right = true; + } + } + + /* check if this is the "oldest" finger down (or the only finger down) + * otherwise it will not affect mouse motion */ + update_pointer = true; + if (num_fingers_down > 1) + { + for (i = 0; i < MAX_NUM_FINGERS; i++) + { + if (sw->finger[i].id != id) + continue; + for (j = 0; j < MAX_NUM_FINGERS; j++) + { + if (sw->finger[j].id == NO_TOUCH || (j == i)) + continue; + if (sw->finger[j].time_last_down < sw->finger[i].time_last_down) + update_pointer = false; + } + } + } + if (!update_pointer) + return; + + if (!sw->touch_mouse_indirect) + { + int x; + int y; + normalized_to_screen_xy(&x, &y, event->tfinger.x, event->tfinger.y); + sw->mouse_x_delta = x - sw->mouse_x; + sw->mouse_y_delta = y - sw->mouse_y; + sw->mouse_x = x; + sw->mouse_y = y; + } + else + { + /* for relative mode, use the pointer speed setting */ + int dx = event->tfinger.dx * TOUCH_MAX_X * 256 * sw->touch_mouse_speed_factor; + int dy = event->tfinger.dy * TOUCH_MAX_Y * 256 * sw->touch_mouse_speed_factor; + sw->hires_dx += dx; + sw->hires_dy += dy; + int x_rel = sw->hires_dx / 256; + int y_rel = sw->hires_dy / 256; + if (x_rel || y_rel) + { + sw->mouse_x_delta = x_rel; + sw->mouse_y_delta = y_rel; + sw->mouse_x += x_rel; + sw->mouse_y += y_rel; + } + sw->hires_dx %= 256; + sw->hires_dy %= 256; + } +} + +void normalized_to_screen_xy(int *screenX, int *screenY, float x, float y) +{ + /* map to display */ + *screenX = x * TOUCH_MAX_X; + *screenY = y * TOUCH_MAX_Y; +} + +void finish_simulated_mouse_clicks(switch_input_t *sw, uint64_t currentTime) +{ + unsigned int i; + for (i = 0; i < 2; i++) + { + if (sw->simulated_click_start_time[i] == 0) + continue; + + if (currentTime - sw->simulated_click_start_time[i] < SIMULATED_CLICK_DURATION) + continue; + + if (i == 0) + sw->mouse_button_left = false; + else + sw->mouse_button_right = false; + + sw->simulated_click_start_time[i] = 0; + } +} +#endif + static void switch_input_free_input(void *data) { switch_input_t *sw = (switch_input_t*) data; @@ -378,11 +802,24 @@ static void* switch_input_init(const char *joypad_driver) input_keymaps_init_keyboard_lut(rarch_key_map_switch); unsigned int i; - for (i = 0; i <= SWITCH_MAX_SCANCODE; i++) { + for (i = 0; i <= SWITCH_MAX_SCANCODE; i++) sw->keyboard_state[i] = false; - } + sw->mouse_x = 0; sw->mouse_y = 0; + sw->mouse_previous_report = 0; + + + /* touch mouse init */ + sw->touch_mouse_indirect = true; /* direct mode is not calibrated it seems */ + sw->touch_mouse_speed_factor = 1.0; + for (i = 0; i < MAX_NUM_FINGERS; i++) + sw->finger[i].id = NO_TOUCH; + + sw->multi_finger_dragging = DRAG_NONE; + + for (i = 0; i < 2; i++) + sw->simulated_click_start_time[i] = 0; #endif return sw;