/* 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/. */ #include "ZenMouseTracker.h" #include "ZenMouseTrackerInternal.h" #include #include "mozilla/Services.h" #include "mozilla/WidgetUtils.h" #include "nsIObserverService.h" #include "nsIWidget.h" #include "nsThreadUtils.h" namespace zen { NS_IMPL_ISUPPORTS(ZenMouseTracker, nsIZenMouseTracker, nsIObserver) static ZenMouseTracker* sInstance = nullptr; // Pointer moves are coalesced: the backends may report them faster than we // want to run the checks, and on Windows they arrive from a low level hook // where as little work as possible should happen. These are atomic since // some backends (e.g. the macOS global event monitor) may deliver the moves // off the main thread. static std::atomic sLatestPointerX{0}; static std::atomic sLatestPointerY{0}; static std::atomic sPendingEvaluation{false}; ZenMouseTracker::ZenMouseTracker() { MOZ_ASSERT(NS_IsMainThread()); sInstance = this; } ZenMouseTracker::~ZenMouseTracker() { ZenNativeMouseMonitor::Stop(); if (sInstance == this) { sInstance = nullptr; } } // static mozilla::Maybe ZenMouseTracker::ParseEdge( const nsACString& aScreenEdge) { if (aScreenEdge.EqualsLiteral("left")) { return mozilla::Some(TrackedEdge::Left); } if (aScreenEdge.EqualsLiteral("right")) { return mozilla::Some(TrackedEdge::Right); } if (aScreenEdge.EqualsLiteral("top")) { return mozilla::Some(TrackedEdge::Top); } if (aScreenEdge.EqualsLiteral("bottom")) { return mozilla::Some(TrackedEdge::Bottom); } return mozilla::Nothing(); } NS_IMETHODIMP ZenMouseTracker::RegisterWindow(mozIDOMWindowProxy* aWindow, const nsACString& aScreenEdge, float aMaxEdgeOffset) { MOZ_ASSERT(NS_IsMainThread()); const auto edge = ParseEdge(aScreenEdge); if (!aWindow || edge.isNothing() || aMaxEdgeOffset < 0.0f) { return NS_ERROR_INVALID_ARG; } nsCOMPtr window = nsPIDOMWindowOuter::From(aWindow); if (!window) { return NS_ERROR_INVALID_ARG; } nsresult rv = ZenNativeMouseMonitor::Start(); if (NS_FAILED(rv)) { return rv; } for (auto& tracked : mTracked) { if (tracked.mWindow == window) { tracked.mEdge = *edge; tracked.mMaxEdgeOffset = aMaxEdgeOffset; return NS_OK; } } mTracked.AppendElement(TrackedWindow{window, *edge, aMaxEdgeOffset}); AddObservers(); return NS_OK; } void ZenMouseTracker::AddObservers() { if (mObserving) { return; } if (nsCOMPtr obs = mozilla::services::GetObserverService()) { obs->AddObserver(this, "domwindowclosed", false); obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); mObserving = true; } } void ZenMouseTracker::RemoveObservers() { if (!mObserving) { return; } if (nsCOMPtr obs = mozilla::services::GetObserverService()) { obs->RemoveObserver(this, "domwindowclosed"); obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); } mObserving = false; } NS_IMETHODIMP ZenMouseTracker::UnregisterWindow(mozIDOMWindowProxy* aWindow) { MOZ_ASSERT(NS_IsMainThread()); if (!aWindow) { return NS_ERROR_INVALID_ARG; } StopTrackingWindow(nsPIDOMWindowOuter::From(aWindow)); return NS_OK; } void ZenMouseTracker::StopTrackingWindow(nsPIDOMWindowOuter* aWindow) { for (size_t i = 0; i < mTracked.Length(); i++) { if (mTracked[i].mWindow == aWindow) { mTracked.RemoveElementAt(i); break; } } OnTrackedListChanged(); } void ZenMouseTracker::OnTrackedListChanged() { if (mTracked.IsEmpty()) { ZenNativeMouseMonitor::Stop(); RemoveObservers(); } } // static void ZenMouseTracker::OnNativePointerMove(const mozilla::DesktopPoint& aPoint) { sLatestPointerX = int32_t(aPoint.x); sLatestPointerY = int32_t(aPoint.y); if (sPendingEvaluation.exchange(true)) { // An already queued evaluation will pick up the position we just stored return; } nsresult rv = NS_DispatchToMainThread( NS_NewRunnableFunction("zen::ZenMouseTracker::Evaluate", [] { sPendingEvaluation = false; if (RefPtr tracker = sInstance) { tracker->Evaluate(mozilla::DesktopPoint(float(sLatestPointerX), float(sLatestPointerY))); } })); if (NS_FAILED(rv)) { // Don't let a failed dispatch (e.g. during shutdown) block all future // evaluations sPendingEvaluation = false; } } void ZenMouseTracker::Evaluate(const mozilla::DesktopPoint& aPoint) { nsTArray> exited; for (size_t i = mTracked.Length(); i > 0; i--) { auto& tracked = mTracked[i - 1]; RefPtr widget = mozilla::widget::WidgetUtils::DOMWindowToWidget(tracked.mWindow); if (!widget) { mTracked.RemoveElementAt(i - 1); continue; } if (!IsPointerWithinBounds(tracked, aPoint, widget)) { exited.AppendElement(std::move(tracked.mWindow)); mTracked.RemoveElementAt(i - 1); } } OnTrackedListChanged(); if (exited.IsEmpty()) { return; } nsCOMPtr obs = mozilla::services::GetObserverService(); if (!obs) { return; } for (auto& window : exited) { obs->NotifyObservers(window, ZEN_MOUSE_TRACKER_EXITED_TOPIC, nullptr); } } // static bool ZenMouseTracker::IsPointerWithinBounds(const TrackedWindow& aTracked, const mozilla::DesktopPoint& aPoint, nsIWidget* aWidget) { const auto scale = aWidget->GetDesktopToDeviceScale(); const float x = aPoint.x * scale.scale; const float y = aPoint.y * scale.scale; const auto origin = aWidget->WidgetToScreenOffset(); const auto size = aWidget->GetClientSize(); const float left = origin.x; const float top = origin.y; const float right = left + size.width; const float bottom = top + size.height; if (x >= left && x <= right && y >= top && y <= bottom) { // Back inside the window; the in-window hover logic owns this case return true; } const float maxOffset = aTracked.mMaxEdgeOffset * float(aWidget->GetDefaultScale().scale); switch (aTracked.mEdge) { case TrackedEdge::Left: return x >= left - maxOffset && x < left && y >= top && y <= bottom; case TrackedEdge::Right: return x > right && x <= right + maxOffset && y >= top && y <= bottom; case TrackedEdge::Top: return y >= top - maxOffset && y < top && x >= left && x <= right; case TrackedEdge::Bottom: return y > bottom && y <= bottom + maxOffset && x >= left && x <= right; } return false; } NS_IMETHODIMP ZenMouseTracker::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp(aTopic, "domwindowclosed")) { if (nsCOMPtr window = do_QueryInterface(aSubject)) { StopTrackingWindow(window); } return NS_OK; } if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { mTracked.Clear(); OnTrackedListChanged(); } return NS_OK; } } // namespace zen