From 36c71c5c2b6a7db07c9744fbc9885cb65c491514 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:32:36 +0200 Subject: [PATCH] gh-14830: Fixed dragging on text makes the entire window to move (gh-14833) --- prefs/zen/compact-mode.yaml | 4 +- prefs/zen/view.yaml | 2 +- src/zen/compact-mode/ZenCompactMode.mjs | 2 +- src/zen/compact-mode/ZenMouseTrackerGtk.cpp | 52 ----------- .../compact-mode/ZenMouseTrackerInternal.h | 7 +- src/zen/compact-mode/moz.build | 4 - src/zen/compact-mode/nsIZenMouseTracker.idl | 2 +- .../actors/ZenWindowDragChild.sys.mjs | 93 +++++++++---------- src/zen/window-drag/components.conf | 13 +++ src/zen/window-drag/moz.build | 19 ++++ src/zen/window-drag/nsIZenWindowDragUtils.idl | 23 +++++ src/zen/window-drag/nsZenWindowDragUtils.cpp | 33 +++++++ src/zen/window-drag/nsZenWindowDragUtils.h | 27 ++++++ 13 files changed, 170 insertions(+), 111 deletions(-) delete mode 100644 src/zen/compact-mode/ZenMouseTrackerGtk.cpp create mode 100644 src/zen/window-drag/components.conf create mode 100644 src/zen/window-drag/nsIZenWindowDragUtils.idl create mode 100644 src/zen/window-drag/nsZenWindowDragUtils.cpp create mode 100644 src/zen/window-drag/nsZenWindowDragUtils.h diff --git a/prefs/zen/compact-mode.yaml b/prefs/zen/compact-mode.yaml index 6f3f862a6..d7de3a472 100644 --- a/prefs/zen/compact-mode.yaml +++ b/prefs/zen/compact-mode.yaml @@ -23,10 +23,10 @@ # 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 + value: 200 - name: zen.view.compact.outside-window-edge-offset.vertical - value: 150 + value: 100 - name: zen.view.compact.animate-sidebar value: true diff --git a/prefs/zen/view.yaml b/prefs/zen/view.yaml index e555e631b..fc433c617 100644 --- a/prefs/zen/view.yaml +++ b/prefs/zen/view.yaml @@ -50,7 +50,7 @@ value: true - name: zen.view.drag-window-from-content.height-percentage - value: 30 + value: 10 - name: zen.view.borderless-fullscreen value: true diff --git a/src/zen/compact-mode/ZenCompactMode.mjs b/src/zen/compact-mode/ZenCompactMode.mjs index 3181f0170..9f85c4125 100644 --- a/src/zen/compact-mode/ZenCompactMode.mjs +++ b/src/zen/compact-mode/ZenCompactMode.mjs @@ -998,7 +998,7 @@ window.gZenCompactModeManager = { try { lazy.zenMouseTracker.registerWindow(window, screenEdge, maxEdgeOffset); } catch (e) { - // The platform can't track the global mouse position (e.g. Wayland) + // The platform can't track the global mouse position (e.g. Linux) return false; } this._outsideTrackedElement = target; diff --git a/src/zen/compact-mode/ZenMouseTrackerGtk.cpp b/src/zen/compact-mode/ZenMouseTrackerGtk.cpp deleted file mode 100644 index 427081603..000000000 --- a/src/zen/compact-mode/ZenMouseTrackerGtk.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* 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) { - GdkDevice* pointer = mozilla::widget::GdkGetPointer(); - 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 index 84bc572e7..80dce4cbc 100644 --- a/src/zen/compact-mode/ZenMouseTrackerInternal.h +++ b/src/zen/compact-mode/ZenMouseTrackerInternal.h @@ -7,7 +7,10 @@ #include "nscore.h" -#if defined(XP_MACOSX) || defined(XP_WIN) || defined(MOZ_WIDGET_GTK) +// On Linux there's no reliable way to observe the global pointer (Wayland +// doesn't expose it at all), so we don't track there and callers fall back +// to their timeout based behavior +#if defined(XP_MACOSX) || defined(XP_WIN) # define NS_ZEN_CAN_TRACK_POINTER 1 #endif @@ -25,7 +28,7 @@ class ZenNativeMouseMonitor final { * @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). + * global pointer (e.g. on Linux). */ static nsresult Start(); /** diff --git a/src/zen/compact-mode/moz.build b/src/zen/compact-mode/moz.build index 93a3d2da8..a4c3dfd11 100644 --- a/src/zen/compact-mode/moz.build +++ b/src/zen/compact-mode/moz.build @@ -33,9 +33,5 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": 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 index 936313827..f78fe1ba0 100644 --- a/src/zen/compact-mode/nsIZenMouseTracker.idl +++ b/src/zen/compact-mode/nsIZenMouseTracker.idl @@ -28,7 +28,7 @@ interface nsIZenMouseTracker : nsISupports { * @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). + * pointer position (e.g. on Linux). */ void registerWindow(in mozIDOMWindowProxy window, in ACString screenEdge, in float maxEdgeOffset); diff --git a/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs b/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs index 1ef721739..672f11ea4 100644 --- a/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs +++ b/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs @@ -10,36 +10,25 @@ XPCOMUtils.defineLazyPreferenceGetter( lazy, "dragRegionHeightPercentage", "zen.view.drag-window-from-content.height-percentage", - 30 + 10 ); -// A small threshold to allow for minor mouse jitter during a normal click. -// Anything beyond this is considered an intentional window drag. -const DRAG_START_THRESHOLD_PX = 4; +XPCOMUtils.defineLazyServiceGetter( + lazy, + "zenWindowDragUtils", + "@mozilla.org/zen/window-drag-utils;1", + Ci.nsIZenWindowDragUtils +); -const kInteractiveTags = new Set([ - "a", - "area", - "audio", - "button", - "canvas", - "details", - "dialog", - "embed", - "frame", - "iframe", - "img", - "input", - "label", - "menu", - "object", - "optgroup", - "option", - "select", - "summary", - "textarea", - "video", -]); +// Movement below this is considered a click, not a window drag. Fast +// clicks commonly slide a few pixels (especially on trackpads), and once +// the native move starts the OS swallows the mouseup — so keep this +// comfortably above click jitter or clicks in the region get lost. +const DRAG_START_THRESHOLD_PX = 10; + +// Content that drives its own mouse interaction without being +// interactive HTML content in the spec sense. +const kAppContentTags = new Set(["audio", "canvas", "video"]); const kInteractiveRoles = new Set([ "button", @@ -282,21 +271,41 @@ export class ZenWindowDragChild extends JSWindowActorChild { return true; } } - return this.#hasInteractiveCursor(target); + return ( + this.#hasInteractiveCursor(target) || this.#isOverSelectableText(event) + ); + } + + /** + * A drag starting over selectable text should select it, not move the + * window. rangeParent is the caret position Gecko computed for the + * event, so this also covers empty space on the same line, where + * dragging extends a selection. + * + * @param {MouseEvent} event + */ + #isOverSelectableText(event) { + const node = event.rangeParent; + if (node?.nodeType !== Node.TEXT_NODE) { + return false; + } + const parent = node.parentElement; + return ( + !parent || + this.contentWindow.getComputedStyle(parent).userSelect !== "none" + ); } #isInteractiveElement(element) { - if (kInteractiveTags.has(element.localName)) { + // Gecko's own notion of interactive, editable or draggable content. + if (lazy.zenWindowDragUtils.isInteractiveContent(element)) { return true; } - // Covers [draggable="true"] and elements draggable by default, - // like links and images. - if (element.draggable) { - return true; - } - if (element.isContentEditable) { + if (kAppContentTags.has(element.localName)) { return true; } + // Declarative signals the engine check doesn't cover: ARIA widget + // roles and explicit tab stops. if ( element.tabIndex >= 0 && element !== this.document.body && @@ -305,19 +314,7 @@ export class ZenWindowDragChild extends JSWindowActorChild { return true; } const role = element.getAttribute?.("role"); - if (role && kInteractiveRoles.has(role.toLowerCase())) { - return true; - } - // Inline event handlers are a strong hint of a custom widget. - if ( - element.onclick || - element.onmousedown || - element.onpointerdown || - element.ondragstart - ) { - return true; - } - return false; + return !!role && kInteractiveRoles.has(role.toLowerCase()); } #hasInteractiveCursor(element) { diff --git a/src/zen/window-drag/components.conf b/src/zen/window-drag/components.conf new file mode 100644 index 000000000..859e63f21 --- /dev/null +++ b/src/zen/window-drag/components.conf @@ -0,0 +1,13 @@ +# 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': '{13df8214-70b4-4d7b-af8c-86081cbc5801}', + 'interfaces': ['nsIZenWindowDragUtils'], + 'contract_ids': ['@mozilla.org/zen/window-drag-utils;1'], + 'type': 'zen::nsZenWindowDragUtils', + 'headers': ['mozilla/nsZenWindowDragUtils.h'], + }, +] diff --git a/src/zen/window-drag/moz.build b/src/zen/window-drag/moz.build index 2b3da954b..888ed0333 100644 --- a/src/zen/window-drag/moz.build +++ b/src/zen/window-drag/moz.build @@ -6,3 +6,22 @@ FINAL_TARGET_FILES.actors += [ "actors/ZenWindowDragChild.sys.mjs", "actors/ZenWindowDragParent.sys.mjs", ] + +XPIDL_SOURCES += [ + "nsIZenWindowDragUtils.idl", +] + +EXPORTS.mozilla += [ + "nsZenWindowDragUtils.h", +] + +SOURCES += [ + "nsZenWindowDragUtils.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +FINAL_LIBRARY = "xul" +XPIDL_MODULE = "zen_window_drag" diff --git a/src/zen/window-drag/nsIZenWindowDragUtils.idl b/src/zen/window-drag/nsIZenWindowDragUtils.idl new file mode 100644 index 000000000..8389bbd01 --- /dev/null +++ b/src/zen/window-drag/nsIZenWindowDragUtils.idl @@ -0,0 +1,23 @@ +/* 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" + +webidl Node; + +/** + * @brief Utilities for deciding when a content-area gesture may move + * the window. + */ +[scriptable, uuid(595ba6e8-abe2-4619-acad-cf492306f608)] +interface nsIZenWindowDragUtils : nsISupports { + /** + * @brief Whether the node is content the page expects the user to + * interact with: interactive HTML content (links, form controls, + * etc.), editable content, or content that can be dragged + * (draggable attribute, links and loaded images). + * @param node The node to check. + */ + boolean isInteractiveContent(in Node node); +}; diff --git a/src/zen/window-drag/nsZenWindowDragUtils.cpp b/src/zen/window-drag/nsZenWindowDragUtils.cpp new file mode 100644 index 000000000..06ef834b3 --- /dev/null +++ b/src/zen/window-drag/nsZenWindowDragUtils.cpp @@ -0,0 +1,33 @@ +/* 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 "nsZenWindowDragUtils.h" + +#include "mozilla/dom/Element.h" +#include "nsContentUtils.h" +#include "nsIContent.h" + +namespace zen { + +NS_IMPL_ISUPPORTS(nsZenWindowDragUtils, nsIZenWindowDragUtils) + +NS_IMETHODIMP +nsZenWindowDragUtils::IsInteractiveContent(nsINode* aNode, bool* aResult) { + *aResult = false; + NS_ENSURE_ARG_POINTER(aNode); + + nsIContent* content = nsIContent::FromNode(aNode); + if (!content) { + return NS_OK; + } + if (content->IsEditable() || nsContentUtils::ContentIsDraggable(content)) { + *aResult = true; + return NS_OK; + } + mozilla::dom::Element* element = mozilla::dom::Element::FromNode(content); + *aResult = element && element->IsInteractiveHTMLContent(); + return NS_OK; +} + +} // namespace zen diff --git a/src/zen/window-drag/nsZenWindowDragUtils.h b/src/zen/window-drag/nsZenWindowDragUtils.h new file mode 100644 index 000000000..ec8195ae9 --- /dev/null +++ b/src/zen/window-drag/nsZenWindowDragUtils.h @@ -0,0 +1,27 @@ +/* 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 zen_nsZenWindowDragUtils_h_ +#define zen_nsZenWindowDragUtils_h_ + +#include "nsIZenWindowDragUtils.h" + +#define ZEN_WINDOW_DRAG_UTILS_CONTRACTID "@mozilla.org/zen/window-drag-utils;1" + +namespace zen { + +class nsZenWindowDragUtils final : public nsIZenWindowDragUtils { + NS_DECL_ISUPPORTS + NS_DECL_NSIZENWINDOWDRAGUTILS + + public: + nsZenWindowDragUtils() = default; + + private: + ~nsZenWindowDragUtils() = default; +}; + +} // namespace zen + +#endif