From 9714c15c7a4edc43ae5e011840a84c12b576f53c Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:24:58 +0200 Subject: [PATCH] gh-13108: Make compact mode track mouse moves outside of window (gh-14818) --- prefs/zen/compact-mode.yaml | 8 + src/zen/compact-mode/ZenCompactMode.mjs | 108 +++++++- src/zen/compact-mode/ZenMouseTracker.cpp | 245 ++++++++++++++++++ src/zen/compact-mode/ZenMouseTracker.h | 92 +++++++ src/zen/compact-mode/ZenMouseTrackerCocoa.mm | 65 +++++ src/zen/compact-mode/ZenMouseTrackerGtk.cpp | 54 ++++ .../compact-mode/ZenMouseTrackerInternal.h | 45 ++++ src/zen/compact-mode/ZenMouseTrackerWin.cpp | 51 ++++ src/zen/compact-mode/components.conf | 14 + src/zen/compact-mode/moz.build | 41 +++ src/zen/compact-mode/nsIZenMouseTracker.idl | 41 +++ src/zen/moz.build | 1 + 12 files changed, 759 insertions(+), 6 deletions(-) create mode 100644 src/zen/compact-mode/ZenMouseTracker.cpp create mode 100644 src/zen/compact-mode/ZenMouseTracker.h create mode 100644 src/zen/compact-mode/ZenMouseTrackerCocoa.mm create mode 100644 src/zen/compact-mode/ZenMouseTrackerGtk.cpp create mode 100644 src/zen/compact-mode/ZenMouseTrackerInternal.h create mode 100644 src/zen/compact-mode/ZenMouseTrackerWin.cpp create mode 100644 src/zen/compact-mode/components.conf create mode 100644 src/zen/compact-mode/moz.build create mode 100644 src/zen/compact-mode/nsIZenMouseTracker.idl diff --git a/prefs/zen/compact-mode.yaml b/prefs/zen/compact-mode.yaml index 829ca7367..6f3f862a6 100644 --- a/prefs/zen/compact-mode.yaml +++ b/prefs/zen/compact-mode.yaml @@ -20,6 +20,14 @@ - name: zen.view.compact.sidebar-keep-hover.duration value: 150 +# How far (in CSS pixels) the mouse may travel past the window bounds after +# leaving the window before the hovered sidebar/toolbar is collapsed +- name: zen.view.compact.outside-window-edge-offset.horizontal + value: 250 + +- name: zen.view.compact.outside-window-edge-offset.vertical + value: 150 + - name: zen.view.compact.animate-sidebar value: true diff --git a/src/zen/compact-mode/ZenCompactMode.mjs b/src/zen/compact-mode/ZenCompactMode.mjs index e33c5d4e7..3181f0170 100644 --- a/src/zen/compact-mode/ZenCompactMode.mjs +++ b/src/zen/compact-mode/ZenCompactMode.mjs @@ -34,6 +34,29 @@ XPCOMUtils.defineLazyPreferenceGetter( true ); +// Distance (in CSS pixels) the mouse can travel past the window bounds after +// leaving the window before the hovered element is collapsed +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "COMPACT_MODE_OUTSIDE_WINDOW_HORIZONTAL_OFFSET", + "zen.view.compact.outside-window-edge-offset.horizontal", + 250 +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "COMPACT_MODE_OUTSIDE_WINDOW_VERTICAL_OFFSET", + "zen.view.compact.outside-window-edge-offset.vertical", + 150 +); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "zenMouseTracker", + "@mozilla.org/zen/mouse-tracker;1", + Ci.nsIZenMouseTracker +); + ChromeUtils.defineLazyGetter(lazy, "mainAppWrapper", () => document.getElementById("zen-main-app-wrapper") ); @@ -71,9 +94,21 @@ window.gZenCompactModeManager = { tabIsRightObserver ); + const outsideMouseTrackerExitObserver = + this._onOutsideMouseTrackerExit.bind(this); + Services.obs.addObserver( + outsideMouseTrackerExitObserver, + "zen-mouse-tracker:exited" + ); + window.addEventListener( "unload", () => { + this._stopTrackingMouseOutsideWindow(); + Services.obs.removeObserver( + outsideMouseTrackerExitObserver, + "zen-mouse-tracker:exited" + ); Services.prefs.removeObserver( "zen.tabs.vertical.right-side", tabIsRightObserver @@ -94,6 +129,10 @@ window.gZenCompactModeManager = { this._clearAllHoverStates() ); + // Hide any element kept open by the outside mouse tracking as soon as the + // window loses focus + window.addEventListener("deactivate", () => this._collapseTrackedElement()); + this._canShowBackgroundTabToast = Services.prefs.getBoolPref( "zen.view.compact.show-background-tab-toast", true @@ -149,6 +188,7 @@ window.gZenCompactModeManager = { return; } delete this._isTabBeingDragged; + this._stopTrackingMouseOutsideWindow(); this.sidebar.removeAttribute("zen-user-show"); // We use this element in order to make it persis across restarts, by using the XULStore. // main-window can't store attributes other than window sizes, so we use this instead @@ -723,6 +763,9 @@ window.gZenCompactModeManager = { } } else { if (attr === "zen-has-hover") { + if (element === this._outsideTrackedElement) { + this._stopTrackingMouseOutsideWindow(); + } element.removeAttribute("zen-has-implicit-hover"); gURLBar.updateTextOverflow(); } @@ -886,18 +929,23 @@ window.gZenCompactModeManager = { } window.cancelAnimationFrame(this._removeHoverFrames[target.id]); - this.flashElement( - target, - this.hideAfterHoverDuration, - "has-hover" + target.id, - "zen-has-hover" - ); + if (!this._trackMouseOutsideWindow(entry.screenEdge, target)) { + // We can't track the mouse position outside of the window on + // this platform, fall back to hiding after a fixed duration + this.flashElement( + target, + this.hideAfterHoverDuration, + "has-hover" + target.id, + "zen-has-hover" + ); + } document.addEventListener( "mousemove", () => { if (target.matches(":hover")) { return; } + // Closing the element also stops the outside mouse tracking this._setElementExpandAttribute(target, false); this.clearFlashTimeout("has-hover" + target.id); }, @@ -941,7 +989,55 @@ window.gZenCompactModeManager = { return bBox.left - error < x && x < bBox.right + error; }, + _trackMouseOutsideWindow(screenEdge, target) { + this._stopTrackingMouseOutsideWindow(); + const maxEdgeOffset = + screenEdge === "left" || screenEdge === "right" + ? lazy.COMPACT_MODE_OUTSIDE_WINDOW_HORIZONTAL_OFFSET + : lazy.COMPACT_MODE_OUTSIDE_WINDOW_VERTICAL_OFFSET; + try { + lazy.zenMouseTracker.registerWindow(window, screenEdge, maxEdgeOffset); + } catch (e) { + // The platform can't track the global mouse position (e.g. Wayland) + return false; + } + this._outsideTrackedElement = target; + this.clearFlashTimeout("has-hover" + target.id); + window.requestAnimationFrame(() => { + if (this._outsideTrackedElement === target) { + this._setElementExpandAttribute(target, true); + } + }); + return true; + }, + + _stopTrackingMouseOutsideWindow() { + const target = this._outsideTrackedElement; + if (!target) { + return; + } + this._outsideTrackedElement = null; + lazy.zenMouseTracker.unregisterWindow(window); + }, + + _collapseTrackedElement() { + const target = this._outsideTrackedElement; + if (!target) { + return; + } + // Closing the element also unregisters us from the mouse tracker + this._setElementExpandAttribute(target, false); + this.clearFlashTimeout("has-hover" + target.id); + }, + + _onOutsideMouseTrackerExit(subject) { + if (subject === window) { + this._collapseTrackedElement(); + } + }, + _clearAllHoverStates() { + this._stopTrackingMouseOutsideWindow(); // Clear hover attributes from all hoverable elements for (let entry of this.hoverableElements) { const target = entry.element; diff --git a/src/zen/compact-mode/ZenMouseTracker.cpp b/src/zen/compact-mode/ZenMouseTracker.cpp new file mode 100644 index 000000000..aa66247f3 --- /dev/null +++ b/src/zen/compact-mode/ZenMouseTracker.cpp @@ -0,0 +1,245 @@ +/* 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 "mozilla/Atomic.h" +#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 mozilla::Atomic sLatestPointerX(0); +static mozilla::Atomic sLatestPointerY(0); +static mozilla::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 diff --git a/src/zen/compact-mode/ZenMouseTracker.h b/src/zen/compact-mode/ZenMouseTracker.h new file mode 100644 index 000000000..bb483eec3 --- /dev/null +++ b/src/zen/compact-mode/ZenMouseTracker.h @@ -0,0 +1,92 @@ +/* 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/. */ + +#ifndef mozilla_ZenMouseTracker_h_ +#define mozilla_ZenMouseTracker_h_ + +#include "nsIZenMouseTracker.h" + +#include "mozilla/Maybe.h" +#include "nsIObserver.h" +#include "nsCOMPtr.h" +#include "nsPIDOMWindow.h" +#include "nsTArray.h" +#include "Units.h" + +class nsIWidget; + +// Fired with the tracked window as subject once the pointer breaks the +// tracking conditions; tracking for that window has already stopped. +#define ZEN_MOUSE_TRACKER_EXITED_TOPIC "zen-mouse-tracker:exited" + +namespace zen { + +/** + * @brief Watches the global OS pointer position for registered windows and + * notifies observers once the pointer moves too far away from the window + * edge it left through. + */ +class ZenMouseTracker final : public nsIZenMouseTracker, public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIZENMOUSETRACKER + NS_DECL_NSIOBSERVER + + ZenMouseTracker(); + + /** + * @brief Called by the platform backends whenever the OS pointer moves. + * @param aPoint The pointer position in desktop pixels, relative to the + * origin of the (primary) screen. May be called outside of a clean event + * loop iteration (e.g. from a low level hook), evaluation is coalesced + * onto the main thread. + */ + static void OnNativePointerMove(const mozilla::DesktopPoint& aPoint); + + private: + ~ZenMouseTracker(); + + enum class TrackedEdge : uint8_t { Left, Right, Top, Bottom }; + + struct TrackedWindow { + nsCOMPtr mWindow; + TrackedEdge mEdge; + float mMaxEdgeOffset; // In CSS pixels + }; + + static mozilla::Maybe ParseEdge(const nsACString& aScreenEdge); + + /** + * @brief Check every tracked window against the given pointer position and + * notify + stop tracking the ones the pointer moved too far away from. + */ + void Evaluate(const mozilla::DesktopPoint& aPoint); + + /** + * @brief Whether the pointer is still allowed to keep the window's edge + * element open, given the window's bounds on screen. + */ + static bool IsPointerWithinBounds(const TrackedWindow& aTracked, + const mozilla::DesktopPoint& aPoint, + nsIWidget* aWidget); + + void StopTrackingWindow(nsPIDOMWindowOuter* aWindow); + + /** + * @brief Tear down the native monitor and our observers once the last + * tracked window is gone, so the service is fully idle while nothing is + * being tracked. + */ + void OnTrackedListChanged(); + + void AddObservers(); + void RemoveObservers(); + + nsTArray mTracked; + bool mObserving = false; +}; + +} // namespace zen + +#endif diff --git a/src/zen/compact-mode/ZenMouseTrackerCocoa.mm b/src/zen/compact-mode/ZenMouseTrackerCocoa.mm new file mode 100644 index 000000000..7c0667caf --- /dev/null +++ b/src/zen/compact-mode/ZenMouseTrackerCocoa.mm @@ -0,0 +1,65 @@ +/* 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 "nsCocoaUtils.h" + +#import +#import + +namespace zen { + +// The global monitor only sees events delivered to other applications, so a +// local monitor is needed as well for moves that AppKit routes to our own +// app while the pointer is outside of the tracked window (AppKit sends mouse +// moved events to the key window regardless of the pointer position). +static id sLocalMonitor = nil; +static id sGlobalMonitor = nil; + +static void ReportPointerPosition() { + NSPoint point = [NSEvent mouseLocation]; + point.y = nsCocoaUtils::FlippedScreenY(point.y); + ZenMouseTracker::OnNativePointerMove( + mozilla::DesktopPoint(float(point.x), float(point.y))); +} + +nsresult ZenNativeMouseMonitor::Start() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + if (sLocalMonitor || sGlobalMonitor) { + return NS_OK; + } + const NSEventMask mask = NSEventMaskMouseMoved | NSEventMaskLeftMouseDragged | + NSEventMaskRightMouseDragged | + NSEventMaskOtherMouseDragged; + sLocalMonitor = + [NSEvent addLocalMonitorForEventsMatchingMask:mask + handler:^(NSEvent* aEvent) { + ReportPointerPosition(); + return aEvent; + }]; + sGlobalMonitor = + [NSEvent addGlobalMonitorForEventsMatchingMask:mask + handler:^(NSEvent* aEvent) { + ReportPointerPosition(); + }]; + return NS_OK; + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_NOT_AVAILABLE); +} + +void ZenNativeMouseMonitor::Stop() { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + if (sLocalMonitor) { + [NSEvent removeMonitor:sLocalMonitor]; + sLocalMonitor = nil; + } + if (sGlobalMonitor) { + [NSEvent removeMonitor:sGlobalMonitor]; + sGlobalMonitor = nil; + } + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +} // namespace zen diff --git a/src/zen/compact-mode/ZenMouseTrackerGtk.cpp b/src/zen/compact-mode/ZenMouseTrackerGtk.cpp new file mode 100644 index 000000000..595af453b --- /dev/null +++ b/src/zen/compact-mode/ZenMouseTrackerGtk.cpp @@ -0,0 +1,54 @@ +/* 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 "mozilla/WidgetUtilsGtk.h" + +#include +#include + +namespace zen { + +// X11 has no way to observe global pointer motion events without pulling in +// extra library dependencies (XInput2 raw events need libXi, XRecord needs +// libXtst), so we poll the pointer from a GLib timeout instead. This only +// runs during the short periods a window is registered, i.e. while the +// pointer is outside of the window with an edge element held open. +static guint sTimerId = 0; + +static gboolean OnTimer(gpointer) { + GdkDisplay* display = gdk_display_get_default(); + GdkSeat* seat = display ? gdk_display_get_default_seat(display) : nullptr; + GdkDevice* pointer = seat ? gdk_seat_get_pointer(seat) : nullptr; + if (pointer) { + gint x = 0, y = 0; + gdk_device_get_position(pointer, nullptr, &x, &y); + ZenMouseTracker::OnNativePointerMove( + mozilla::DesktopPoint(float(x), float(y))); + } + return G_SOURCE_CONTINUE; +} + +nsresult ZenNativeMouseMonitor::Start() { + if (!mozilla::widget::GdkIsX11Display()) { + // Wayland doesn't expose the pointer position while it is outside of our + // surfaces + return NS_ERROR_NOT_AVAILABLE; + } + if (!sTimerId) { + sTimerId = g_timeout_add(1000 / 60, OnTimer, nullptr); + } + return NS_OK; +} + +void ZenNativeMouseMonitor::Stop() { + if (sTimerId) { + g_source_remove(sTimerId); + sTimerId = 0; + } +} + +} // namespace zen diff --git a/src/zen/compact-mode/ZenMouseTrackerInternal.h b/src/zen/compact-mode/ZenMouseTrackerInternal.h new file mode 100644 index 000000000..84bc572e7 --- /dev/null +++ b/src/zen/compact-mode/ZenMouseTrackerInternal.h @@ -0,0 +1,45 @@ +/* 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/. */ + +#ifndef mozilla_ZenMouseTrackerInternal_h_ +#define mozilla_ZenMouseTrackerInternal_h_ + +#include "nscore.h" + +#if defined(XP_MACOSX) || defined(XP_WIN) || defined(MOZ_WIDGET_GTK) +# define NS_ZEN_CAN_TRACK_POINTER 1 +#endif + +namespace zen { + +/** + * @brief Platform backend that delivers global pointer movements to + * ZenMouseTracker::OnNativePointerMove, including movements outside of any + * of our windows. + */ +class ZenNativeMouseMonitor final { + public: +#ifdef NS_ZEN_CAN_TRACK_POINTER + /** + * @brief Start delivering pointer moves. Safe to call while already + * started. + * @throws NS_ERROR_NOT_AVAILABLE when the platform cannot observe the + * global pointer (e.g. on Wayland). + */ + static nsresult Start(); + /** + * @brief Stop delivering pointer moves. Safe to call while stopped. + */ + static void Stop(); +#else + static nsresult Start() { return NS_ERROR_NOT_AVAILABLE; } + static void Stop() {} +#endif + + ZenNativeMouseMonitor() = delete; +}; + +} // namespace zen + +#endif diff --git a/src/zen/compact-mode/ZenMouseTrackerWin.cpp b/src/zen/compact-mode/ZenMouseTrackerWin.cpp new file mode 100644 index 000000000..8a3d73d55 --- /dev/null +++ b/src/zen/compact-mode/ZenMouseTrackerWin.cpp @@ -0,0 +1,51 @@ +/* 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 + +namespace zen { + +static HHOOK sMouseHook = nullptr; + +// Runs on the thread that installed the hook (the main thread). Keep this as +// light as possible: the system silently removes hooks that take too long. +static LRESULT CALLBACK MouseHookProc(int aCode, WPARAM aWParam, + LPARAM aLParam) { + if (aCode == HC_ACTION && aWParam == WM_MOUSEMOVE) { + const auto* info = reinterpret_cast(aLParam); + // Screen physical pixels, which are what desktop pixels map to on Windows + ZenMouseTracker::OnNativePointerMove( + mozilla::DesktopPoint(float(info->pt.x), float(info->pt.y))); + } + return ::CallNextHookEx(nullptr, aCode, aWParam, aLParam); +} + +nsresult ZenNativeMouseMonitor::Start() { + if (sMouseHook) { + return NS_OK; + } + // Pass the module that actually contains the hook proc (xul.dll), not the + // executable's module + HMODULE module = nullptr; + if (!::GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&MouseHookProc), + &module)) { + return NS_ERROR_NOT_AVAILABLE; + } + sMouseHook = ::SetWindowsHookExW(WH_MOUSE_LL, MouseHookProc, module, 0); + return sMouseHook ? NS_OK : NS_ERROR_NOT_AVAILABLE; +} + +void ZenNativeMouseMonitor::Stop() { + if (sMouseHook) { + ::UnhookWindowsHookEx(sMouseHook); + sMouseHook = nullptr; + } +} + +} // namespace zen diff --git a/src/zen/compact-mode/components.conf b/src/zen/compact-mode/components.conf new file mode 100644 index 000000000..6e0d7df25 --- /dev/null +++ b/src/zen/compact-mode/components.conf @@ -0,0 +1,14 @@ +# 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/. + +Classes = [ + { + 'cid': '{03d900f7-639f-4c81-b029-2883dd893ccb}', + 'interfaces': ['nsIZenMouseTracker'], + 'contract_ids': ['@mozilla.org/zen/mouse-tracker;1'], + 'type': 'zen::ZenMouseTracker', + 'headers': ['mozilla/ZenMouseTracker.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, +] diff --git a/src/zen/compact-mode/moz.build b/src/zen/compact-mode/moz.build new file mode 100644 index 000000000..93a3d2da8 --- /dev/null +++ b/src/zen/compact-mode/moz.build @@ -0,0 +1,41 @@ +# 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/. + +XPIDL_SOURCES += [ + "nsIZenMouseTracker.idl", +] + +EXPORTS.mozilla += [ + "ZenMouseTracker.h", + "ZenMouseTrackerInternal.h", +] + +SOURCES += [ + "ZenMouseTracker.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +LOCAL_INCLUDES += [ + "/widget", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + SOURCES += ["ZenMouseTrackerCocoa.mm"] + LOCAL_INCLUDES += [ + "/widget/cocoa", + "/xpcom/base", + ] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + SOURCES += ["ZenMouseTrackerWin.cpp"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + SOURCES += ["ZenMouseTrackerGtk.cpp"] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +FINAL_LIBRARY = "xul" +XPIDL_MODULE = "zen_compact_mode" diff --git a/src/zen/compact-mode/nsIZenMouseTracker.idl b/src/zen/compact-mode/nsIZenMouseTracker.idl new file mode 100644 index 000000000..936313827 --- /dev/null +++ b/src/zen/compact-mode/nsIZenMouseTracker.idl @@ -0,0 +1,41 @@ +/* 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 "nsISupports.idl" + +interface mozIDOMWindowProxy; + +/** + * @brief Tracks the OS pointer for windows the pointer has left. + * Once the pointer either crosses the window bounds on the + * axis perpendicular to the registered edge, or moves further than the + * given offset past that edge, the "zen-mouse-tracker:exited" observer + * notification is fired with the window as subject and tracking for that + * window stops. + */ +[scriptable, uuid(023e85e6-44e1-46b3-9cfb-6f3c7fa075f8)] +interface nsIZenMouseTracker : nsISupports { + /** + * @brief Start tracking the pointer for the given window. + * + * Re-registering a window updates its edge and offset. Tracking is + * automatically cleared when the window is closed. + * + * @param window The window the pointer just left. + * @param screenEdge The edge the pointer left through, one of "left", + * "right", "top" or "bottom". + * @param maxEdgeOffset How far (in CSS pixels) the pointer may travel past + * screenEdge before the exit notification is fired. + * @throws NS_ERROR_NOT_AVAILABLE when the platform cannot track the global + * pointer position (e.g. on Wayland). + */ + void registerWindow(in mozIDOMWindowProxy window, in ACString screenEdge, + in float maxEdgeOffset); + + /** + * @brief Stop tracking the pointer for the given window. No-op if the + * window isn't being tracked. + */ + void unregisterWindow(in mozIDOMWindowProxy window); +}; diff --git a/src/zen/moz.build b/src/zen/moz.build index 4e3e80ecb..87a233ebb 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -9,6 +9,7 @@ EXTRA_PP_COMPONENTS += [ DIRS += [ "boosts", "common", + "compact-mode", "drag-and-drop", "glance", "live-folders",