diff --git a/README.md b/README.md
index ee6978a272..3f6d6b68fd 100644
--- a/README.md
+++ b/README.md
@@ -546,6 +546,19 @@ into the device clipboard. As a consequence, any Android application could read
its content. You should avoid to paste sensitive content (like passwords) that
way.
+
+#### Pinch-to-zoom
+
+To simulate "pinch-to-zoom": Ctrl+_click-and-move_.
+
+More precisely, hold Ctrl while pressing the left-click button. Until
+the left-click button is released, all mouse movements scale and rotate the
+content (if supported by the app) relative to the center of the screen.
+
+Concretely, scrcpy generates additional touch events from a "virtual finger" at
+a location inverted through the center of the screen.
+
+
#### Text injection preference
There are two kinds of [events][textevents] generated when typing text:
@@ -659,6 +672,7 @@ _[Super] is typically the Windows or Cmd key._
| Synchronize clipboards and paste³ | MOD+v
| Inject computer clipboard text | MOD+Shift+v
| Enable/disable FPS counter (on stdout) | MOD+i
+ | Pinch-to-zoom | Ctrl+_click-and-move_
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
diff --git a/app/scrcpy.1 b/app/scrcpy.1
index 49fa78dc73..4f3a8b9cc5 100644
--- a/app/scrcpy.1
+++ b/app/scrcpy.1
@@ -316,6 +316,10 @@ Inject computer clipboard text as a sequence of key events
.B MOD+i
Enable/disable FPS counter (print frames/second in logs)
+.TP
+.B Ctrl+click-and-move
+Pinch-to-zoom from the center of the screen
+
.TP
.B Drag & drop APK file
Install APK from computer
diff --git a/app/src/cli.c b/app/src/cli.c
index c957b22fef..c960727eb0 100644
--- a/app/src/cli.c
+++ b/app/src/cli.c
@@ -279,6 +279,9 @@ scrcpy_print_usage(const char *arg0) {
" MOD+i\n"
" Enable/disable FPS counter (print frames/second in logs)\n"
"\n"
+ " Ctrl+click-and-move\n"
+ " Pinch-to-zoom from the center of the screen\n"
+ "\n"
" Drag & drop APK file\n"
" Install APK from computer\n"
"\n",
diff --git a/app/src/control_msg.h b/app/src/control_msg.h
index e0b480de40..6e3f239c91 100644
--- a/app/src/control_msg.h
+++ b/app/src/control_msg.h
@@ -17,6 +17,7 @@
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6)
#define POINTER_ID_MOUSE UINT64_C(-1);
+#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2);
enum control_msg_type {
CONTROL_MSG_TYPE_INJECT_KEYCODE,
diff --git a/app/src/input_manager.c b/app/src/input_manager.c
index 1d73980cae..962db1d3e7 100644
--- a/app/src/input_manager.c
+++ b/app/src/input_manager.c
@@ -70,6 +70,8 @@ input_manager_init(struct input_manager *im,
im->sdl_shortcut_mods.data[i] = sdl_mod;
}
im->sdl_shortcut_mods.count = shortcut_mods->count;
+
+ im->vfinger_down = false;
}
static void
@@ -299,6 +301,36 @@ input_manager_process_text_input(struct input_manager *im,
}
}
+static bool
+simulate_virtual_finger(struct input_manager *im,
+ enum android_motionevent_action action,
+ struct point point) {
+ bool up = action == AMOTION_EVENT_ACTION_UP;
+
+ struct control_msg msg;
+ msg.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
+ msg.inject_touch_event.action = action;
+ msg.inject_touch_event.position.screen_size = im->screen->frame_size;
+ msg.inject_touch_event.position.point = point;
+ msg.inject_touch_event.pointer_id = POINTER_ID_VIRTUAL_FINGER;
+ msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
+ msg.inject_touch_event.buttons = 0;
+
+ if (!controller_push_msg(im->controller, &msg)) {
+ LOGW("Could not request 'inject virtual finger event'");
+ return false;
+ }
+
+ return true;
+}
+
+static struct point
+inverse_point(struct point point, struct size size) {
+ point.x = size.width - point.x;
+ point.y = size.height - point.y;
+ return point;
+}
+
static bool
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
bool prefer_text, uint32_t repeat) {
@@ -512,10 +544,18 @@ input_manager_process_mouse_motion(struct input_manager *im,
return;
}
struct control_msg msg;
- if (convert_mouse_motion(event, im->screen, &msg)) {
- if (!controller_push_msg(im->controller, &msg)) {
- LOGW("Could not request 'inject mouse motion event'");
- }
+ if (!convert_mouse_motion(event, im->screen, &msg)) {
+ return;
+ }
+
+ if (!controller_push_msg(im->controller, &msg)) {
+ LOGW("Could not request 'inject mouse motion event'");
+ }
+
+ if (im->vfinger_down) {
+ struct point mouse = msg.inject_touch_event.position.point;
+ struct point vfinger = inverse_point(mouse, im->screen->frame_size);
+ simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger);
}
}
@@ -587,7 +627,9 @@ input_manager_process_mouse_button(struct input_manager *im,
// simulated from touch events, so it's a duplicate
return;
}
- if (event->type == SDL_MOUSEBUTTONDOWN) {
+
+ bool down = event->type == SDL_MOUSEBUTTONDOWN;
+ if (down) {
if (control && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(im->controller);
return;
@@ -618,10 +660,36 @@ input_manager_process_mouse_button(struct input_manager *im,
}
struct control_msg msg;
- if (convert_mouse_button(event, im->screen, &msg)) {
- if (!controller_push_msg(im->controller, &msg)) {
- LOGW("Could not request 'inject mouse button event'");
+ if (!convert_mouse_button(event, im->screen, &msg)) {
+ return;
+ }
+
+ if (!controller_push_msg(im->controller, &msg)) {
+ LOGW("Could not request 'inject mouse button event'");
+ return;
+ }
+
+ // Pinch-to-zoom simulation.
+ //
+ // If Ctrl is hold when the left-click button is pressed, then
+ // pinch-to-zoom mode is enabled: on every mouse event until the left-click
+ // button is released, an additional "virtual finger" event is generated,
+ // having a position inverted through the center of the screen.
+ //
+ // In other words, the center of the rotation/scaling is the center of the
+ // screen.
+#define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL))
+ if ((down && !im->vfinger_down && CTRL_PRESSED)
+ || (!down && im->vfinger_down)) {
+ struct point mouse = msg.inject_touch_event.position.point;
+ struct point vfinger = inverse_point(mouse, im->screen->frame_size);
+ enum android_motionevent_action action = down
+ ? AMOTION_EVENT_ACTION_DOWN
+ : AMOTION_EVENT_ACTION_UP;
+ if (!simulate_virtual_finger(im, action, vfinger)) {
+ return;
}
+ im->vfinger_down = down;
}
}
diff --git a/app/src/input_manager.h b/app/src/input_manager.h
index 8811c4578a..c3756e40cf 100644
--- a/app/src/input_manager.h
+++ b/app/src/input_manager.h
@@ -30,6 +30,8 @@ struct input_manager {
unsigned data[SC_MAX_SHORTCUT_MODS];
unsigned count;
} sdl_shortcut_mods;
+
+ bool vfinger_down;
};
void