diff --git a/src/external-patches/firefox/expose_tiled_attribute_to_all_platforms/D312028.patch b/src/external-patches/firefox/expose_tiled_attribute_to_all_platforms/D312028.patch new file mode 100644 index 000000000..905b694ce --- /dev/null +++ b/src/external-patches/firefox/expose_tiled_attribute_to_all_platforms/D312028.patch @@ -0,0 +1,136 @@ +diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css +--- a/browser/themes/linux/browser.css ++++ b/browser/themes/linux/browser.css +@@ -32,11 +32,11 @@ + */ + @media (-moz-gtk-csd-transparency-available) { + :root[customtitlebar] { + background-color: transparent; + +- &[sizemode="normal"]:not([gtktiledwindow]) { ++ &[sizemode="normal"]:not([tiled]) { + /* Firefox draws its contents to a child window, while GTK takes care of + * drawing the toplevel (which in most cases is just the window + * decorations). + * + * Due to how X11 child windows work, pixels painted by a child window will +diff --git a/browser/themes/shared/browser-shared.css b/browser/themes/shared/browser-shared.css +--- a/browser/themes/shared/browser-shared.css ++++ b/browser/themes/shared/browser-shared.css +@@ -433,18 +433,18 @@ + } + } + + @media (-moz-platform: linux) and (-moz-gtk-csd-reversed-placement: 0) { + :root:not([sizemode="normal"]) &[type="pre-tabs"], +- :root[gtktiledwindow] &[type="pre-tabs"] { ++ :root[tiled] &[type="pre-tabs"] { + display: none; + } + } + + @media (-moz-gtk-csd-reversed-placement) { + :root:not([sizemode="normal"]) &[type="post-tabs"], +- :root[gtktiledwindow] &[type="post-tabs"] { ++ :root[tiled] &[type="post-tabs"] { + display: none; + } + } + + @media (max-width: 500px) { +diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp +--- a/widget/gtk/nsWindow.cpp ++++ b/widget/gtk/nsWindow.cpp +@@ -3683,11 +3683,11 @@ + #ifdef ACCESSIBILITY + DispatchRestoreEventAccessible(); + #endif // ACCESSIBILITY + } + +- mIsTiled = aEvent->new_window_state & GDK_WINDOW_STATE_TILED; ++ SetIsTiled(aEvent->new_window_state & GDK_WINDOW_STATE_TILED); + LOG("\tTiled: %d\n", int(mIsTiled)); + mResizableEdges = [&] { + Sides result; + if (mSizeMode != nsSizeMode_Normal) { + return result; +diff --git a/widget/nsIWidget.h b/widget/nsIWidget.h +--- a/widget/nsIWidget.h ++++ b/widget/nsIWidget.h +@@ -1379,10 +1379,12 @@ + + protected: + // Returns whether compositing should use an external surface size. + virtual bool UseExternalCompositingSurface() const { return false; } + ++ void SetIsTiled(bool); ++ + /** + * Starts the OMTC compositor destruction sequence. + * + * When this function returns, the compositor should not be + * able to access the opengl context anymore. +diff --git a/widget/nsIWidget.cpp b/widget/nsIWidget.cpp +--- a/widget/nsIWidget.cpp ++++ b/widget/nsIWidget.cpp +@@ -327,10 +327,16 @@ + void nsIWidget::QuitIME() { + IMEStateManager::WidgetOnQuit(this); + this->mIMEHasQuit = true; + } + ++void nsIWidget::SetIsTiled(bool aIsTiled) { ++ // TODO: Do we want to report an event here or something when stuff changes? ++ // For now this is propagated in a somewhat out-of-band way via AppWindow. ++ mIsTiled = aIsTiled; ++} ++ + void nsIWidget::DestroyCompositor() { + RevokeTransactionIdAllocator(); + + // We release this before releasing the compositor, since it may hold the + // last reference to our ClientLayerManager. ClientLayerManager's dtor can +diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py +--- a/xpcom/ds/StaticAtoms.py ++++ b/xpcom/ds/StaticAtoms.py +@@ -1512,11 +1512,10 @@ + Atom("gamma", "gamma"), + Atom("glyphRef", "glyphRef"), + Atom("grad", "grad"), + Atom("gradientTransform", "gradientTransform"), + Atom("gradientUnits", "gradientUnits"), +- Atom("gtktiledwindow", "gtktiledwindow"), + Atom("hardLight", "hard-light"), + Atom("hue", "hue"), + Atom("hueRotate", "hueRotate"), + Atom("identity", "identity"), + Atom("image_rendering", "image-rendering"), +@@ -1950,10 +1949,11 @@ + Atom("superscriptshift", "superscriptshift"), + Atom("symmetric", "symmetric"), + Atom("tanh", "tanh"), + Atom("tan", "tan"), + Atom("tendsto", "tendsto"), ++ Atom("tiled", "tiled"), + Atom("times", "times"), + Atom("transpose", "transpose"), + Atom("union_", "union"), + Atom("uplimit", "uplimit"), + Atom("variance", "variance"), +diff --git a/xpfe/appshell/AppWindow.cpp b/xpfe/appshell/AppWindow.cpp +--- a/xpfe/appshell/AppWindow.cpp ++++ b/xpfe/appshell/AppWindow.cpp +@@ -1992,11 +1992,11 @@ + aRootElement.SetAttr(nsGkAtoms::sizemode, sizeString, IgnoreErrors()); + if (aShouldPersist && aPersistString.Find(u"sizemode") >= 0) { + (void)SetPersistentValue(nsGkAtoms::sizemode, sizeString); + } + } +- aRootElement.SetBoolAttr(nsGkAtoms::gtktiledwindow, mWindow->IsTiled()); ++ aRootElement.SetBoolAttr(nsGkAtoms::tiled, mWindow->IsTiled()); + } + + void AppWindow::SavePersistentAttributes( + const PersistentAttributes aAttributes) { + // can happen when the persistence timer fires at an inopportune time + diff --git a/src/external-patches/firefox/expose_tiled_attribute_to_all_platforms/D312036.patch b/src/external-patches/firefox/expose_tiled_attribute_to_all_platforms/D312036.patch new file mode 100644 index 000000000..da5bd2a7e --- /dev/null +++ b/src/external-patches/firefox/expose_tiled_attribute_to_all_platforms/D312036.patch @@ -0,0 +1,18 @@ +diff --git a/widget/windows/nsWindow.cpp b/widget/windows/nsWindow.cpp +--- a/widget/windows/nsWindow.cpp ++++ b/widget/windows/nsWindow.cpp +@@ -6423,10 +6423,13 @@ + // Skip window size change events below on minimization. + return; + } + } + ++ // Recompute tiled state. ++ SetIsTiled(mWnd && ::IsWindowArranged(mWnd)); ++ + // Notify visibility change when window is activated. + if (!(wp->flags & SWP_NOACTIVATE) && NeedsToTrackWindowOcclusionState()) { + WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged( + this, mFrameState->GetSizeMode() != nsSizeMode_Minimized); + } + diff --git a/src/external-patches/firefox/expose_tiled_attribute_to_all_platforms/D312091.patch b/src/external-patches/firefox/expose_tiled_attribute_to_all_platforms/D312091.patch new file mode 100644 index 000000000..c57ec7208 --- /dev/null +++ b/src/external-patches/firefox/expose_tiled_attribute_to_all_platforms/D312091.patch @@ -0,0 +1,72 @@ +diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm +--- a/widget/cocoa/nsCocoaWindow.mm ++++ b/widget/cocoa/nsCocoaWindow.mm +@@ -70,10 +70,11 @@ + #include "nsDocShell.h" + + #include "gfxPlatform.h" + #include "qcms.h" + ++#import + #include "mozilla/AutoRestore.h" + #include "mozilla/BasicEvents.h" + #include "mozilla/dom/Document.h" + #include "mozilla/Maybe.h" + #include "mozilla/NativeKeyBindingsType.h" +@@ -6948,10 +6949,31 @@ + return nsSizeMode_Maximized; + } + return nsSizeMode_Normal; + } + ++// The tiling state lives in a private NSWindow ivar "_tilingStateController" ++// (an _NSWMWindowTilingStateController). That controller's -tilingState returns ++// a non-nil _WMWindowTilingState only while the window is tiled by the system ++// window manager. None of this is public API, so it is all guarded and degrades ++// to false if the internals ever change. ++static bool WindowIsTiled(NSWindow* aWindow) { ++ if (!aWindow) { ++ return false; ++ } ++ static Ivar sControllerIvar = ++ class_getInstanceVariable([NSWindow class], "_tilingStateController"); ++ if (!sControllerIvar) { ++ return false; ++ } ++ id controller = object_getIvar(aWindow, sControllerIvar); ++ if (![controller respondsToSelector:@selector(tilingState)]) { ++ return false; ++ } ++ return [controller performSelector:@selector(tilingState)] != nil; ++} ++ + void nsCocoaWindow::ReportMoveEvent() { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + // Prevent recursion, which can become infinite (see bug 708278). This + // can happen when the call to [NSWindow setFrameTopLeftPoint:] in +@@ -6961,10 +6983,11 @@ + return; + } + mInReportMoveEvent = true; + + UpdateBounds(); ++ SetIsTiled(WindowIsTiled(mWindow)); + + // The zoomed state can change when we're moving, in which case we need to + // update our internal mSizeMode. This can happen either if we're maximized + // and then moved, or if we're not maximized and moved back to zoomed state. + if (mWindow && (mSizeMode == nsSizeMode_Maximized) ^ mWindow.isZoomed) { +@@ -7055,10 +7078,11 @@ + + void nsCocoaWindow::ReportSizeEvent() { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + UpdateBounds(); ++ SetIsTiled(WindowIsTiled(mWindow)); + LayoutDeviceIntRect innerBounds = GetClientBounds(); + if (mWidgetListener) { + mWidgetListener->WindowResized(this, innerBounds.Size()); + } + if (mAttachedWidgetListener) { + diff --git a/src/external-patches/manifest.json b/src/external-patches/manifest.json index 8f89a255f..bcc92b779 100644 --- a/src/external-patches/manifest.json +++ b/src/external-patches/manifest.json @@ -39,5 +39,14 @@ "type": "phabricator", "id": "D291714", "name": "gh-12979 Clip dirty_rect to device_size" + }, + { + "type": "phabricator", + "ids": [ + "D312028", + "D312036", + "D312091" + ], + "name": "Expose tiled attribute to all platforms" } ] diff --git a/src/zen/common/modules/ZenSidebarNotification.mjs b/src/zen/common/modules/ZenSidebarNotification.mjs index f5e134ffa..81d7fcade 100644 --- a/src/zen/common/modules/ZenSidebarNotification.mjs +++ b/src/zen/common/modules/ZenSidebarNotification.mjs @@ -22,27 +22,64 @@ class ZenSidebarNotification extends MozLitElement { static properties = { headingL10nId: { type: String, fluent: true }, links: { type: Array }, + autoHideMs: { type: Number }, }; - constructor({ headingL10nId = "", links = [] } = {}) { + #autoHideAnimation = null; + + constructor({ headingL10nId = "", links = [], autoHideMs = 0 } = {}) { super(); this.headingL10nId = headingL10nId; this.links = links; + this.autoHideMs = autoHideMs; + // Hold the countdown while the user is reading. + this.addEventListener("mouseenter", () => this.#autoHideAnimation?.pause()); + this.addEventListener("mouseleave", () => this.#autoHideAnimation?.play()); } connectedCallback() { super.connectedCallback(); if (this.parentElement) { - this.#animateIn(); + this.#animateIn().then(() => this.#startAutoHide()); } } remove() { + if (this.#autoHideAnimation?.playState !== "finished") { + // Stop a running countdown; a finished one must keep its fill so + // the bar doesn't snap back to full during the fade-out. + this.#autoHideAnimation?.cancel(); + } + this.#autoHideAnimation = null; this.#animateOut().then(() => { super.remove(); }); } + async #startAutoHide() { + if (!this.autoHideMs || !this.isConnected) { + return; + } + await this.updateComplete; + const bar = this.shadowRoot?.querySelector( + ".zen-sidebar-notification-progress-bar" + ); + if (!bar || this.#autoHideAnimation) { + return; + } + this.#autoHideAnimation = bar.animate( + { transform: ["scaleX(1)", "scaleX(0)"] }, + { duration: this.autoHideMs, easing: "linear", fill: "forwards" } + ); + this.#autoHideAnimation.finished.then( + () => this.remove(), + () => {} + ); + if (this.matches(":hover")) { + this.#autoHideAnimation.pause(); + } + } + render() { return html` + ${ + this.autoHideMs + ? html` +
+
+
+ ` + : null + }
${this.links.map( link => html` diff --git a/src/zen/common/modules/ZenUpdates.mjs b/src/zen/common/modules/ZenUpdates.mjs index c1f512fd6..f0631e022 100644 --- a/src/zen/common/modules/ZenUpdates.mjs +++ b/src/zen/common/modules/ZenUpdates.mjs @@ -7,6 +7,7 @@ import createSidebarNotification from "chrome://browser/content/zen-components/Z const ZEN_UPDATE_PREF = "zen.updates.last-version"; const ZEN_BUILD_ID_PREF = "zen.updates.last-build-id"; const ZEN_UPDATE_SHOW = "zen.updates.show-update-notification"; +const ZEN_UPDATE_NOTIFICATION_TIMEOUT_MS = 15000; export default function checkForZenUpdates() { const version = Services.appinfo.version; @@ -25,6 +26,7 @@ export default function checkForZenUpdates() { ); createSidebarNotification({ headingL10nId: "zen-sidebar-notification-updated-heading", + autoHideMs: ZEN_UPDATE_NOTIFICATION_TIMEOUT_MS, links: [ { url: Services.urlFormatter.formatURL( diff --git a/src/zen/common/styles/zen-sidebar-notification.css b/src/zen/common/styles/zen-sidebar-notification.css index a5e15194d..2348e32a0 100644 --- a/src/zen/common/styles/zen-sidebar-notification.css +++ b/src/zen/common/styles/zen-sidebar-notification.css @@ -77,6 +77,18 @@ } } +.zen-sidebar-notification-progress { + height: 2px; + overflow: hidden; + transform: translateY(-1px); + + & .zen-sidebar-notification-progress-bar { + height: 100%; + background: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb)); + transform-origin: left center; + } +} + .zen-sidebar-notification-body { padding: 6px; display: flex; diff --git a/src/zen/media/ZenMediaController.mjs b/src/zen/media/ZenMediaController.mjs index 4d2adc0cf..95f39786f 100644 --- a/src/zen/media/ZenMediaController.mjs +++ b/src/zen/media/ZenMediaController.mjs @@ -12,7 +12,7 @@ XPCOMUtils.defineLazyPreferenceGetter( // Maximum number of cards shown in the stack; extra cards stay hidden until // a slot frees up. -const MAX_STACKED_CARDS = 5; +const MAX_STACKED_CARDS = 3; // How many background cards visually peek out at the top of the stack. // Deeper cards hide perfectly behind the last peeking one, so the stack // doesn't take more room with every extra card. @@ -412,7 +412,25 @@ class ZenMediaCard { this.#tabTimeout = null; } - this.element.remove(); + const { element } = this; + if (element.hidden) { + element.remove(); + } else { + // Animate the card out instead of popping it away. It leaves the + // flex flow immediately (so the remaining cards re-slot right away) + // but stays anchored where it currently is while it sinks and fades. + const barBottom = + this.manager.mediaControlBar.getBoundingClientRect().bottom; + element.style.bottom = + barBottom - element.getBoundingClientRect().bottom + "px"; + element.setAttribute("zen-removing", "true"); + // Resolves once the exit transitions finish or get cancelled, and + // immediately if they never start (e.g. reduced motion). + Promise.allSettled( + element.getAnimations().map(animation => animation.finished) + ).then(() => element.remove()); + } + this.manager.onCardDestroyed(this); } } @@ -452,21 +470,28 @@ class nsZenMediaController { window.addEventListener("DOMAudioPlaybackStarted", event => { const browser = event.target; - // Only create the card if the media is still playing a moment - // later, so short sounds (e.g. notification pings) never produce - // one. + // The card is created right away (while the controller is fresh) + // but only shown if the media is still playing a moment later, so + // short sounds (e.g. notification pings) never reveal it. setTimeout(() => { - const mediaController = browser.browsingContext?.mediaController; - if (!mediaController?.isPlaying) { + const card = this.#cardForBrowser(browser); + if (!card || card.isSharing) { return; } - - this.activateMediaControls(mediaController, browser); - const card = this.#cardForBrowser(browser); - if (card && !card.isSharing) { + if (card.controller.isPlaying) { card.refreshVisibility(); + } else if (card.element.hidden) { + // The sound was over before ever being shown (e.g. a + // notification ping): drop the card instead of keeping a ghost + // around. + card.destroy(); } }, 1000); + + this.activateMediaControls( + browser.browsingContext.mediaController, + browser + ); }); window.addEventListener("DOMAudioPlaybackStopped", () => { @@ -623,18 +648,31 @@ class nsZenMediaController { card => !card.element.hidden ); + // Re-slot the indices without transitions first: the index transform + // only compensates the flex-order shift, so a reindexed card keeps + // its visual position through this step. visibleCards.forEach((card, index) => { card.element.toggleAttribute( "stack-overflow", index >= MAX_STACKED_CARDS ); card.element.toggleAttribute("stacked-behind", index > 0); + card.element.setAttribute("zen-reindexing", "true"); card.element.style.setProperty("--zen-media-card-index", index); + card.element.style.zIndex = 100 - index; + }); + + // Flush styles so the re-slotting lands instantly... + this.mediaControlBar.getBoundingClientRect(); + + // ...then animate only the peek level: demoted cards slide up from + // behind the front card instead of dropping in from above. + visibleCards.forEach((card, index) => { + card.element.removeAttribute("zen-reindexing"); card.element.style.setProperty( "--zen-media-card-peek-level", Math.min(index, MAX_PEEK_LEVELS) ); - card.element.style.zIndex = 100 - index; }); const stackedCount = Math.min(visibleCards.length, MAX_STACKED_CARDS); diff --git a/src/zen/media/zen-media-controls.css b/src/zen/media/zen-media-controls.css index cd99f79d8..045afa8f9 100644 --- a/src/zen/media/zen-media-controls.css +++ b/src/zen/media/zen-media-controls.css @@ -9,6 +9,7 @@ --button-spacing: 2px; --zen-media-stack-peek: 8px; --zen-media-stack-gap: 6px; + --zen-media-stack-easing: cubic-bezier(0.25, 1, 0.5, 1); display: flex; min-width: 0; @@ -88,7 +89,7 @@ animation: zen-back-and-forth-text 10s infinite ease-in-out; } - & .zen-media-card { + & .zen-media-card:not([zen-removing]) { transform: none; opacity: 1; } @@ -114,10 +115,10 @@ padding: 0 6px; pointer-events: none; transition: - max-height 0.2s ease, - opacity 0.2s ease, - transform 0.2s ease, - padding 0.2s ease; + max-height 0.3s var(--zen-media-stack-easing), + opacity 0.3s var(--zen-media-stack-easing), + transform 0.3s var(--zen-media-stack-easing), + padding 0.3s var(--zen-media-stack-easing); } .zen-media-current-time, @@ -149,12 +150,39 @@ ) ) scale(calc(1 - 0.04 * var(--zen-media-card-peek-level, 0))); - opacity: calc(1 - 0.4 * var(--zen-media-card-peek-level, 0)); + opacity: calc(1 - 0.3 * var(--zen-media-card-peek-level, 0)); + filter: blur(0); transition: - transform 0.1s ease-out, - opacity 0.2s ease-out, + transform 0.35s var(--zen-media-stack-easing), + opacity 0.3s var(--zen-media-stack-easing), + filter 0.3s var(--zen-media-stack-easing), padding 0.3s ease-out; + @starting-style { + opacity: 0; + transform: translateY(0.5rem); + filter: blur(4px); + } + + &[zen-reindexing] { + transition: none; + } + + &[zen-removing] { + position: absolute; + left: 0; + width: 100%; + transform: none; + translate: 0 0.5rem; + opacity: 0; + filter: blur(2px); + pointer-events: none; + transition: + translate 0.3s var(--zen-media-stack-easing), + opacity 0.3s var(--zen-media-stack-easing), + filter 0.3s var(--zen-media-stack-easing); + } + &[stack-overflow] { display: none; } @@ -249,7 +277,7 @@ overflow: visible; position: relative; z-index: 2; - transition: height 0.2s ease-out; + transition: height 0.3s var(--zen-media-stack-easing); } } diff --git a/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs b/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs index 94fd85f08..39d813ff1 100644 --- a/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs +++ b/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs @@ -26,6 +26,10 @@ XPCOMUtils.defineLazyServiceGetter( // comfortably above click jitter or clicks in the region get lost. const DRAG_START_THRESHOLD_PX = 4; +// Un-snapping a maximized or tiled window is more disruptive, so those +// need a firmer gesture, mirroring how native titlebars behave. +const SNAPPED_DRAG_START_THRESHOLD_PX = 50; + // Content that drives its own mouse interaction without being // interactive HTML content in the spec sense. const kAppContentTags = new Set(["audio", "canvas", "video"]); @@ -63,6 +67,9 @@ export class ZenWindowDragChild extends JSWindowActorChild { #dragging = false; #startScreenX = 0; #startScreenY = 0; + // 0 while waiting for the ZenWindowDrag:IsSnapped answer; no drag can + // start until the proper threshold is known. + #dragThresholdPx = 0; handleEvent(event) { // Never let pages spoof the gesture with synthetic events. @@ -145,8 +152,18 @@ export class ZenWindowDragChild extends JSWindowActorChild { const { screenX, screenY } = this.#screenPoint(event); this.#startScreenX = screenX; this.#startScreenY = screenY; + this.#dragThresholdPx = 0; this.#tracking = true; this.#addGestureListeners(); + this.sendQuery("ZenWindowDrag:IsSnapped") + .then(snapped => { + if (this.#tracking) { + this.#dragThresholdPx = snapped + ? SNAPPED_DRAG_START_THRESHOLD_PX + : DRAG_START_THRESHOLD_PX; + } + }) + .catch(() => this.#reset()); } #onMouseMove(event) { @@ -164,9 +181,12 @@ export class ZenWindowDragChild extends JSWindowActorChild { event.preventDefault(); return; } + if (!this.#dragThresholdPx) { + return; + } const point = this.#screenPoint(event); const threshold = - DRAG_START_THRESHOLD_PX * this.contentWindow.devicePixelRatio; + this.#dragThresholdPx * this.contentWindow.devicePixelRatio; if ( Math.hypot( point.screenX - this.#startScreenX, diff --git a/src/zen/window-drag/actors/ZenWindowDragParent.sys.mjs b/src/zen/window-drag/actors/ZenWindowDragParent.sys.mjs index 3d92ab614..5866bc94f 100644 --- a/src/zen/window-drag/actors/ZenWindowDragParent.sys.mjs +++ b/src/zen/window-drag/actors/ZenWindowDragParent.sys.mjs @@ -15,19 +15,42 @@ XPCOMUtils.defineLazyServiceGetter( export class ZenWindowDragParent extends JSWindowActorParent { receiveMessage(message) { - if (message.name !== "ZenWindowDrag:StartDrag") { - return; - } const win = this.browsingContext.topChromeWindow; - if (!win || win.closed || win.windowState === win.STATE_FULLSCREEN) { - return; + if (!win || win.closed) { + return undefined; } - if (Cu.isInAutomation) { - // Tests can't exercise a real OS drag session; let them observe the - // decision instead. - Services.obs.notifyObservers(win, "zen-window-drag-started"); - return; + switch (message.name) { + case "ZenWindowDrag:StartDrag": { + if (win.windowState === win.STATE_FULLSCREEN) { + break; + } + if (Cu.isInAutomation) { + // Tests can't exercise a real OS drag session; let them observe + // the decision instead. + Services.obs.notifyObservers(win, "zen-window-drag-started"); + break; + } + lazy.zenDragAndDropService.beginNativeWindowMove(win); + break; + } + case "ZenWindowDrag:IsSnapped": { + return this.#isSnapped(win); + } } - lazy.zenDragAndDropService.beginNativeWindowMove(win); + return undefined; + } + + /** + * Whether the window is maximized or tiled to an edge. The tiled + * attribute is kept in sync with the widget by AppWindow, the same way + * sizemode is. + * + * @param {ChromeWindow} win + */ + #isSnapped(win) { + return ( + win.windowState === win.STATE_MAXIMIZED || + win.document.documentElement.hasAttribute("tiled") + ); } } diff --git a/surfer.json b/surfer.json index dc8662fbc..1eadb5aea 100644 --- a/surfer.json +++ b/surfer.json @@ -20,7 +20,7 @@ "brandShortName": "Zen", "brandFullName": "Zen Browser", "release": { - "displayVersion": "1.21.12b", + "displayVersion": "1.21.13b", "github": { "repo": "zen-browser/desktop" },