Bug 1416316 - 2. Clean up NativePanZoomController; r?rbarker draft
authorJim Chen <nchen@mozilla.com>
Thu, 30 Nov 2017 13:25:50 -0500
changeset 705811 0e3da7bfd2f6b3e64c5cc1f0ba659cadc7ba5aa7
parent 705810 dc203200e659482ea14de71cd5fa67728132c5fd
child 705812 3ec173cee30ffdbedf6ca408cb8f4ff52f8a3974
push id91595
push userbmo:nchen@mozilla.com
push dateThu, 30 Nov 2017 18:26:05 +0000
reviewersrbarker
bugs1416316
milestone59.0a1
Bug 1416316 - 2. Clean up NativePanZoomController; r?rbarker Clean up the NativePanZoomController object including, 1) Remove references to LayerView because NPZC will be created and used by LayerSession. 2) Rename `mDestroyed` to `mAttached` because NPZC now needs to support cases where it's used before being attached, in addition to after being destroyed. 3) Move origin of synthesized event coordinates from the screen to the surface in native code, so we don't need to do the same thing in Java. 4) Invoke all callbacks from native code on the UI thread. MozReview-Commit-ID: Fu4XIY59yKw
mobile/android/base/moz.build
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
widget/android/nsWindow.cpp
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -401,17 +401,16 @@ gvjar.sources += [geckoview_source_dir +
     'gfx/GeckoSurface.java',
     'gfx/GeckoSurfaceTexture.java',
     'gfx/IntSize.java',
     'gfx/LayerSession.java',
     'gfx/LayerView.java',
     'gfx/NativePanZoomController.java',
     'gfx/OverscrollEdgeEffect.java',
     'gfx/PanningPerfAPI.java',
-    'gfx/PanZoomController.java',
     'gfx/PointUtils.java',
     'gfx/RenderTask.java',
     'gfx/StackScroller.java',
     'gfx/SurfaceAllocator.java',
     'gfx/SurfaceAllocatorService.java',
     'gfx/SurfaceTextureListener.java',
     'gfx/ViewTransform.java',
     'gfx/VsyncSource.java',
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
@@ -1,41 +1,35 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.gfx;
 
 import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
 import org.mozilla.gecko.mozglue.JNIObject;
 import org.mozilla.gecko.util.ThreadUtils;
 
-import org.json.JSONObject;
-
-import android.graphics.PointF;
+import android.graphics.Matrix;
+import android.graphics.Rect;
 import android.os.SystemClock;
 import android.util.Log;
-import android.util.TypedValue;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.View;
 import android.view.InputDevice;
 
 import java.util.ArrayList;
 
-class NativePanZoomController extends JNIObject implements PanZoomController {
+public class NativePanZoomController extends JNIObject {
     private static final String LOGTAG = "GeckoNPZC";
-    private final float MAX_SCROLL;
 
-    private final LayerView mView;
-
-    private boolean mDestroyed;
-    private float mPointerScrollFactor;
+    private final LayerSession mSession;
+    private final Rect mTempRect = new Rect();
+    private boolean mAttached;
+    private float mPointerScrollFactor = 64.0f;
     private long mLastDownTime;
 
     private SynthesizedEventState mPointerState;
 
     @WrapForJNI(calledFrom = "ui")
     private native boolean handleMotionEvent(
             int action, int actionIndex, long time, int metaState,
             int pointerId[], float x[], float y[], float orientation[], float pressure[],
@@ -48,17 +42,17 @@ class NativePanZoomController extends JN
             float hScroll, float vScroll);
 
     @WrapForJNI(calledFrom = "ui")
     private native boolean handleMouseEvent(
             int action, long time, int metaState,
             float x, float y, int buttons);
 
     private boolean handleMotionEvent(MotionEvent event) {
-        if (mDestroyed) {
+        if (!mAttached) {
             return false;
         }
 
         final int action = event.getActionMasked();
         final int count = event.getPointerCount();
 
         if (action == MotionEvent.ACTION_DOWN) {
             mLastDownTime = event.getDownTime();
@@ -92,156 +86,199 @@ class NativePanZoomController extends JN
         }
 
         return handleMotionEvent(action, event.getActionIndex(), event.getEventTime(),
                 event.getMetaState(), pointerId, x, y, orientation, pressure,
                 toolMajor, toolMinor);
     }
 
     private boolean handleScrollEvent(MotionEvent event) {
-        if (mDestroyed) {
+        if (!mAttached) {
             return false;
         }
 
         final int count = event.getPointerCount();
 
         if (count <= 0) {
             return false;
         }
 
         final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
         event.getPointerCoords(0, coords);
-        final float x = coords.x;
-        // Scroll events are not adjusted by the AndroidDyanmicToolbarAnimator so adjust the offset here.
-        final int toolbarHeight = (mView.mSession != null) ?
-            mView.mSession.getDynamicToolbarAnimator().getCurrentToolbarHeight() : 0;
-        final float y = coords.y - toolbarHeight;
+
+        // Translate surface origin to client origin for scroll events.
+        mSession.getSurfaceBounds(mTempRect);
+        final float x = coords.x - mTempRect.left;
+        final float y = coords.y - mTempRect.top;
 
         final float hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL) *
                               mPointerScrollFactor;
         final float vScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) *
                               mPointerScrollFactor;
 
-        return handleScrollEvent(event.getEventTime(), event.getMetaState(), x, y, hScroll, vScroll);
+        return handleScrollEvent(event.getEventTime(), event.getMetaState(), x, y,
+                                 hScroll, vScroll);
     }
 
     private boolean handleMouseEvent(MotionEvent event) {
-        if (mDestroyed) {
+        if (!mAttached) {
             return false;
         }
 
         final int count = event.getPointerCount();
 
         if (count <= 0) {
             return false;
         }
 
         final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
         event.getPointerCoords(0, coords);
-        final float x = coords.x;
-        // Mouse events are not adjusted by the AndroidDyanmicToolbarAnimator so adjust the offset
-        // here.
-        final int toolbarHeight = (mView.mSession != null) ?
-            mView.mSession.getDynamicToolbarAnimator().getCurrentToolbarHeight() : 0;
-        final float y = coords.y - toolbarHeight;
+
+        // Translate surface origin to client origin for mouse events.
+        mSession.getSurfaceBounds(mTempRect);
+        final float x = coords.x - mTempRect.left;
+        final float y = coords.y - mTempRect.top;
 
-        return handleMouseEvent(event.getActionMasked(), event.getEventTime(), event.getMetaState(), x, y, event.getButtonState());
+        return handleMouseEvent(event.getActionMasked(), event.getEventTime(),
+                                event.getMetaState(), x, y, event.getButtonState());
+    }
+
+    /* package */ NativePanZoomController(final LayerSession session) {
+        mSession = session;
     }
 
-
-    NativePanZoomController(View view) {
-        MAX_SCROLL = 0.075f * view.getContext().getResources().getDisplayMetrics().densityDpi;
-
-        mView = (LayerView) view;
-
-        TypedValue outValue = new TypedValue();
-        if (view.getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
-            mPointerScrollFactor = outValue.getDimension(view.getContext().getResources().getDisplayMetrics());
-        } else {
-            mPointerScrollFactor = MAX_SCROLL;
-        }
+    /**
+     * Set the current scroll factor. The scroll factor is the maximum scroll amount that
+     * one scroll event may generate, in device pixels.
+     *
+     * @return Scroll factor.
+     */
+    public void setScrollFactor(final float factor) {
+        ThreadUtils.assertOnUiThread();
+        mPointerScrollFactor = factor;
     }
 
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-// NOTE: This commented out block of code allows Fennec to generate
-//       mouse event instead of converting them to touch events.
-//       This gives Fennec similar behaviour to desktop when using
-//       a mouse.
-//
-//        if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
-//            return handleMouseEvent(event);
-//        } else {
-//            return handleMotionEvent(event);
-//        }
+    /**
+     * Get the current scroll factor.
+     *
+     * @return Scroll factor.
+     */
+    public float getScrollFactor() {
+        ThreadUtils.assertOnUiThread();
+        return mPointerScrollFactor;
+    }
+
+    /**
+     * Process a touch event through the pan-zoom controller. Treat any mouse events as
+     * "touch" rather than as "mouse". Pointer coordinates should be relative to the
+     * display surface.
+     *
+     * @param event MotionEvent to process.
+     * @return True if the event was handled.
+     */
+    public boolean onTouchEvent(final MotionEvent event) {
+        ThreadUtils.assertOnUiThread();
         return handleMotionEvent(event);
     }
 
-    @Override
+    /**
+     * Process a touch event through the pan-zoom controller. Treat any mouse events as
+     * "mouse" rather than as "touch". Pointer coordinates should be relative to the
+     * display surface.
+     *
+     * @param event MotionEvent to process.
+     * @return True if the event was handled.
+     */
+    public boolean onMouseEvent(final MotionEvent event) {
+        ThreadUtils.assertOnUiThread();
+
+        if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
+            return handleMouseEvent(event);
+        }
+        return handleMotionEvent(event);
+    }
+
+    /**
+     * Process a non-touch motion event through the pan-zoom controller. Currently, hover
+     * and scroll events are supported. Pointer coordinates should be relative to the
+     * display surface.
+     *
+     * @param event MotionEvent to process.
+     * @return True if the event was handled.
+     */
     public boolean onMotionEvent(MotionEvent event) {
+        ThreadUtils.assertOnUiThread();
+
         final int action = event.getActionMasked();
         if (action == MotionEvent.ACTION_SCROLL) {
             if (event.getDownTime() >= mLastDownTime) {
                 mLastDownTime = event.getDownTime();
-            } else if ((InputDevice.getDevice(event.getDeviceId()).getSources() & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD) {
+            } else if ((InputDevice.getDevice(event.getDeviceId()).getSources() &
+                        InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD) {
                 return false;
             }
             return handleScrollEvent(event);
         } else if ((action == MotionEvent.ACTION_HOVER_MOVE) ||
                    (action == MotionEvent.ACTION_HOVER_ENTER) ||
                    (action == MotionEvent.ACTION_HOVER_EXIT)) {
             return handleMouseEvent(event);
         } else {
             return false;
         }
     }
 
-    @Override @WrapForJNI(calledFrom = "ui") // PanZoomController
-    public void destroy() {
-        if (mDestroyed || !mView.isCompositorReady()) {
-            return;
+    @WrapForJNI(calledFrom = "ui")
+    private void setAttached(final boolean attached) {
+        if (attached) {
+            mAttached = true;
+        } else if (mAttached) {
+            mAttached = false;
+            disposeNative();
         }
-        mDestroyed = true;
-        disposeNative();
     }
 
-    @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko_priority") @Override // JNIObject
+    @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") @Override // JNIObject
     protected native void disposeNative();
 
     @WrapForJNI(stubName = "SetIsLongpressEnabled") // Called from test thread.
     private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled);
 
-    @Override // PanZoomController
+    /**
+     * Set whether Gecko should generate long-press events.
+     *
+     * @param isLongpressEnabled True if Gecko should generate long-press events.
+     */
     public void setIsLongpressEnabled(boolean isLongpressEnabled) {
-        if (!mDestroyed) {
+        ThreadUtils.assertOnUiThread();
+
+        if (mAttached) {
             nativeSetIsLongpressEnabled(isLongpressEnabled);
         }
     }
 
-
     private static class PointerInfo {
         // We reserve one pointer ID for the mouse, so that tests don't have
         // to worry about tracking pointer IDs if they just want to test mouse
         // event synthesization. If somebody tries to use this ID for a
         // synthesized touch event we'll throw an exception.
         public static final int RESERVED_MOUSE_POINTER_ID = 100000;
 
         public int pointerId;
         public int source;
-        public int screenX;
-        public int screenY;
+        public int surfaceX;
+        public int surfaceY;
         public double pressure;
         public int orientation;
 
         public MotionEvent.PointerCoords getCoords() {
             MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
             coords.orientation = orientation;
             coords.pressure = (float)pressure;
-            coords.x = screenX;
-            coords.y = screenY;
+            coords.x = surfaceX;
+            coords.y = surfaceY;
             return coords;
         }
     }
 
     private static class SynthesizedEventState {
         public final ArrayList<PointerInfo> pointers;
         public long downTime;
 
@@ -307,25 +344,19 @@ class NativePanZoomController extends JN
                     coords[index++] = pointers.get(i).getCoords();
                 }
             }
             return coords;
         }
     }
 
     private void synthesizeNativePointer(int source, int pointerId, int eventType,
-                                         int screenX, int screenY, double pressure,
+                                         int clientX, int clientY, double pressure,
                                          int orientation)
     {
-        final View view = mView;
-        final int[] origin = new int[2];
-        view.getLocationOnScreen(origin);
-        screenX -= origin[0];
-        screenY -= origin[1];
-
         if (mPointerState == null) {
             mPointerState = new SynthesizedEventState();
         }
 
         // Find the pointer if it already exists
         int pointerIndex = mPointerState.getPointerIndex(pointerId);
 
         // Event-specific handling
@@ -369,20 +400,25 @@ class NativePanZoomController extends JN
                     pointerIndex = mPointerState.addPointer(pointerId, source);
                 } else {
                     // We're moving an existing mouse pointer that went down.
                     eventType = MotionEvent.ACTION_MOVE;
                 }
                 break;
         }
 
+        // Translate client origin to surface origin.
+        mSession.getSurfaceBounds(mTempRect);
+        final int surfaceX = clientX + mTempRect.left;
+        final int surfaceY = clientY + mTempRect.top;
+
         // Update the pointer with the new info
         PointerInfo info = mPointerState.pointers.get(pointerIndex);
-        info.screenX = screenX;
-        info.screenY = screenY;
+        info.surfaceX = surfaceX;
+        info.surfaceY = surfaceY;
         info.pressure = pressure;
         info.orientation = orientation;
 
         // Dispatch the event
         int action = 0;
         if (eventType == MotionEvent.ACTION_POINTER_DOWN ||
             eventType == MotionEvent.ACTION_POINTER_UP) {
             // for pointer-down and pointer-up events we need to add the
@@ -404,43 +440,37 @@ class NativePanZoomController extends JN
             /*metaState*/ 0,
             /*buttonState*/ (isButtonDown ? MotionEvent.BUTTON_PRIMARY : 0),
             /*xPrecision*/ 0,
             /*yPrecision*/ 0,
             /*deviceId*/ 0,
             /*edgeFlags*/ 0,
             /*source*/ source,
             /*flags*/ 0);
-        view.post(new Runnable() {
-            @Override
-            public void run() {
-                view.dispatchTouchEvent(event);
-            }
-        });
+        onTouchEvent(event);
 
         // Forget about removed pointers
         if (eventType == MotionEvent.ACTION_POINTER_UP ||
             eventType == MotionEvent.ACTION_UP ||
             eventType == MotionEvent.ACTION_CANCEL ||
             eventType == MotionEvent.ACTION_HOVER_MOVE)
         {
             mPointerState.pointers.remove(pointerIndex);
         }
     }
 
-    @WrapForJNI(calledFrom = "gecko")
-    private void synthesizeNativeTouchPoint(int pointerId, int eventType, int screenX,
-            int screenY, double pressure, int orientation)
-    {
+    @WrapForJNI(calledFrom = "ui")
+    private void synthesizeNativeTouchPoint(int pointerId, int eventType, int clientX,
+                                            int clientY, double pressure, int orientation) {
         if (pointerId == PointerInfo.RESERVED_MOUSE_POINTER_ID) {
             throw new IllegalArgumentException("Pointer ID reserved for mouse");
         }
         synthesizeNativePointer(InputDevice.SOURCE_TOUCHSCREEN, pointerId,
-                                eventType, screenX, screenY, pressure, orientation);
+                                eventType, clientX, clientY, pressure, orientation);
     }
 
-    @WrapForJNI(calledFrom = "gecko")
-    private void synthesizeNativeMouseEvent(int eventType, int screenX, int screenY) {
+    @WrapForJNI(calledFrom = "ui")
+    private void synthesizeNativeMouseEvent(int eventType, int clientX, int clientY) {
         synthesizeNativePointer(InputDevice.SOURCE_MOUSE,
                                 PointerInfo.RESERVED_MOUSE_POINTER_ID,
-                                eventType, screenX, screenY, 0, 0);
+                                eventType, clientX, clientY, 0, 0);
     }
 }
deleted file mode 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gfx;
-
-import android.graphics.PointF;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-
-public interface PanZoomController {
-    static class Factory {
-        static PanZoomController create(View view) {
-            return new NativePanZoomController(view);
-        }
-    }
-
-    public void destroy();
-
-    public boolean onTouchEvent(MotionEvent event);
-    public boolean onMotionEvent(MotionEvent event);
-
-    public void setIsLongpressEnabled(boolean isLongpressEnabled);
-}
--- a/widget/android/nsWindow.cpp
+++ b/widget/android/nsWindow.cpp
@@ -427,17 +427,17 @@ public:
         //
         // 7) is solved by taking a lock whenever mWindow is modified on the
         // Gecko thread or accessed on the UI thread. That way, we don't
         // release mWindow until the UI thread is done using it, thus avoiding
         // the race condition.
 
         typedef NativePanZoomController::GlobalRef NPZCRef;
         auto callDestroy = [] (const NPZCRef& npzc) {
-            npzc->Destroy();
+            npzc->SetAttached(false);
         };
 
         NativePanZoomController::GlobalRef npzc = mNPZC;
         RefPtr<nsThread> uiThread = GetAndroidUiThread();
         if (!uiThread) {
             return;
         }
         uiThread->Dispatch(NewRunnableFunction(
@@ -900,21 +900,28 @@ public:
     void AttachToJava(jni::Object::Param aNPZC)
     {
         MOZ_ASSERT(NS_IsMainThread());
         if (!mWindow) {
             return; // Already shut down.
         }
 
         MOZ_ASSERT(aNPZC);
+        MOZ_ASSERT(!mWindow->mNPZCSupport);
+
         auto npzc = NativePanZoomController::LocalRef(
                 jni::GetGeckoThreadEnv(),
                 NativePanZoomController::Ref::From(aNPZC));
         mWindow->mNPZCSupport.Attach(npzc, mWindow, npzc);
 
+        DispatchToUiThread("LayerViewSupport::AttachNPZC",
+                           [npzc = NativePanZoomController::GlobalRef(npzc)] {
+                                npzc->SetAttached(true);
+                           });
+
         if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
             LayerSession::Compositor::GlobalRef compositor(mCompositor);
             uiThread->Dispatch(NS_NewRunnableFunction(
                     "LayerViewSupport::AttachToJava",
                     [compositor] {
                         compositor->OnCompositorAttached();
                     }));
         }
@@ -1353,16 +1360,17 @@ nsWindow::GeckoViewSupport::Close()
 
 void
 nsWindow::GeckoViewSupport::Transfer(const GeckoSession::Window::LocalRef& inst,
                                      jni::Object::Param aCompositor,
                                      jni::Object::Param aDispatcher,
                                      jni::Object::Param aSettings)
 {
     if (window.mNPZCSupport) {
+        MOZ_ASSERT(window.mLayerViewSupport);
         window.mNPZCSupport.Detach();
     }
 
     if (window.mLayerViewSupport) {
         window.mLayerViewSupport.Detach();
     }
 
     auto compositor = LayerSession::Compositor::LocalRef(
@@ -2240,45 +2248,75 @@ nsWindow::SynthesizeNativeTouchPoint(uin
         break;
     case TOUCH_HOVER:   // not supported for now
     default:
         return NS_ERROR_UNEXPECTED;
     }
 
     MOZ_ASSERT(mNPZCSupport);
     const auto& npzc = mNPZCSupport->GetJavaNPZC();
-    npzc->SynthesizeNativeTouchPoint(aPointerId, eventType, aPoint.x, aPoint.y,
-                                     aPointerPressure, aPointerOrientation);
+    const auto& bounds = FindTopLevel()->mBounds;
+    aPoint.x -= bounds.x;
+    aPoint.y -= bounds.y;
+
+    DispatchToUiThread(
+            "nsWindow::SynthesizeNativeTouchPoint",
+            [npzc = NativePanZoomController::GlobalRef(npzc),
+             aPointerId, eventType, aPoint,
+             aPointerPressure, aPointerOrientation] {
+                npzc->SynthesizeNativeTouchPoint(aPointerId, eventType,
+                                                 aPoint.x, aPoint.y,
+                                                 aPointerPressure,
+                                                 aPointerOrientation);
+            });
     return NS_OK;
 }
 
 nsresult
 nsWindow::SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint,
                                      uint32_t aNativeMessage,
                                      uint32_t aModifierFlags,
                                      nsIObserver* aObserver)
 {
     mozilla::widget::AutoObserverNotifier notifier(aObserver, "mouseevent");
 
     MOZ_ASSERT(mNPZCSupport);
     const auto& npzc = mNPZCSupport->GetJavaNPZC();
-    npzc->SynthesizeNativeMouseEvent(aNativeMessage, aPoint.x, aPoint.y);
+    const auto& bounds = FindTopLevel()->mBounds;
+    aPoint.x -= bounds.x;
+    aPoint.y -= bounds.y;
+
+    DispatchToUiThread(
+            "nsWindow::SynthesizeNativeMouseEvent",
+            [npzc = NativePanZoomController::GlobalRef(npzc),
+             aNativeMessage, aPoint] {
+                npzc->SynthesizeNativeMouseEvent(aNativeMessage,
+                                                 aPoint.x, aPoint.y);
+            });
     return NS_OK;
 }
 
 nsresult
 nsWindow::SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
                                     nsIObserver* aObserver)
 {
     mozilla::widget::AutoObserverNotifier notifier(aObserver, "mouseevent");
 
     MOZ_ASSERT(mNPZCSupport);
     const auto& npzc = mNPZCSupport->GetJavaNPZC();
-    npzc->SynthesizeNativeMouseEvent(sdk::MotionEvent::ACTION_HOVER_MOVE,
-                                     aPoint.x, aPoint.y);
+    const auto& bounds = FindTopLevel()->mBounds;
+    aPoint.x -= bounds.x;
+    aPoint.y -= bounds.y;
+
+    DispatchToUiThread(
+            "nsWindow::SynthesizeNativeMouseMove",
+            [npzc = NativePanZoomController::GlobalRef(npzc), aPoint] {
+                npzc->SynthesizeNativeMouseEvent(sdk::MotionEvent::ACTION_HOVER_MOVE,
+                                                 aPoint.x, aPoint.y);
+            });
     return NS_OK;
 }
 
 bool
 nsWindow::WidgetPaintsBackground()
 {
     static bool sWidgetPaintsBackground = true;
     static bool sWidgetPaintsBackgroundPrefCached = false;