mirror of
https://github.com/zen-browser/desktop.git
synced 2026-08-28 01:11:32 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0069716cd9 | ||
|
|
0953d0cda0 | ||
|
|
864a8ef713 | ||
|
|
759aa967b3 | ||
|
|
2a887a5f2e | ||
|
|
73ed322695 | ||
|
|
e7a3e1ee29 |
@@ -48,6 +48,11 @@ zen-look-and-feel-compact-view-top-toolbar =
|
||||
zen-look-and-feel-compact-toolbar-flash-popup =
|
||||
.label = Briefly make the toolbar popup when switching or opening new tabs in compact mode
|
||||
|
||||
zen-look-and-feel-window-drag-header = Window dragging
|
||||
zen-look-and-feel-window-drag-description = Move the window by dragging empty space at the top of websites, just like the titlebar.
|
||||
zen-window-drag-enabled =
|
||||
.label = Allow dragging the window from web pages
|
||||
|
||||
pane-zen-tabs-title = Tab Management
|
||||
category-zen-workspaces =
|
||||
.tooltiptext = { pane-zen-tabs-title }
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
value: true
|
||||
|
||||
- name: zen.view.drag-window-from-content.height-percentage
|
||||
value: 10
|
||||
value: 5
|
||||
|
||||
- name: zen.view.borderless-fullscreen
|
||||
value: true
|
||||
|
||||
@@ -1184,6 +1184,11 @@ Preferences.addAll([
|
||||
type: "bool",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: "zen.view.drag-window-from-content",
|
||||
type: "bool",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: "zen.urlbar.behavior",
|
||||
type: "string",
|
||||
|
||||
@@ -61,6 +61,15 @@
|
||||
data-l10n-id="zen-look-and-feel-compact-toolbar-flash-popup"
|
||||
preference="zen.view.compact.toolbar-flash-popup"/>
|
||||
</vbox>
|
||||
|
||||
<label><html:h2 data-l10n-id="zen-look-and-feel-window-drag-header"/></label>
|
||||
<description class="description-deemphasized" data-l10n-id="zen-look-and-feel-window-drag-description" />
|
||||
|
||||
<vbox class="indent">
|
||||
<checkbox id="zenLooksAndFeelWindowDragEnabled"
|
||||
data-l10n-id="zen-window-drag-enabled"
|
||||
preference="zen.view.drag-window-from-content"/>
|
||||
</vbox>
|
||||
</groupbox>
|
||||
|
||||
<hbox id="zenGlanceCategory"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 <objc/runtime.h>
|
||||
#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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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`
|
||||
<link
|
||||
@@ -62,6 +99,15 @@ class ZenSidebarNotification extends MozLitElement {
|
||||
<img src="chrome://browser/skin/zen-icons/close.svg" />
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
this.autoHideMs
|
||||
? html`
|
||||
<div class="zen-sidebar-notification-progress">
|
||||
<div class="zen-sidebar-notification-progress-bar"></div>
|
||||
</div>
|
||||
`
|
||||
: null
|
||||
}
|
||||
<div class="zen-sidebar-notification-body">
|
||||
${this.links.map(
|
||||
link => html`
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -262,7 +262,7 @@
|
||||
light-dark(white, black)
|
||||
);
|
||||
--zen-sidebar-notification-shadow: 0 0 6px light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.3));
|
||||
--zen-sidebar-notification-outline: 1px solid light-dark(transparent, rgba(255, 255, 255, 0.15));
|
||||
--zen-sidebar-notification-outline: 1px solid light-dark(transparent, rgba(255, 255, 255, 0.1));
|
||||
|
||||
--zen-sidebar-themed-icon-fill: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -46,6 +46,10 @@ class ZenMediaCard {
|
||||
this.#initListeners();
|
||||
|
||||
if (controller) {
|
||||
// Queried before anything is wired up: it throws if the controller
|
||||
// went away in the meantime, and the caller drops the card.
|
||||
const positionState = controller.getPositionState();
|
||||
|
||||
this.#controllerListeners = {
|
||||
positionstatechange: this.#onPositionstateChange.bind(this),
|
||||
playbackstatechange: this.#onPlaybackstateChange.bind(this),
|
||||
@@ -62,7 +66,7 @@ class ZenMediaCard {
|
||||
|
||||
this.element.classList.toggle("playing", controller.isPlaying);
|
||||
this.updateMetadata();
|
||||
this.updatePositionState(controller.getPositionState());
|
||||
this.updatePositionState(positionState);
|
||||
this.updateSupportedKeys();
|
||||
} else {
|
||||
this.element.setAttribute("media-sharing", "");
|
||||
@@ -164,11 +168,33 @@ class ZenMediaCard {
|
||||
}
|
||||
|
||||
setHidden(hidden) {
|
||||
if (this.element.hidden === hidden) {
|
||||
if (!hidden) {
|
||||
// Showing cancels a hide that may still be animating.
|
||||
this.element.removeAttribute("zen-hiding");
|
||||
if (this.element.hidden) {
|
||||
this.element.hidden = false;
|
||||
this.manager.onCardVisibilityChanged();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.element.hidden = hidden;
|
||||
this.manager.onCardVisibilityChanged();
|
||||
|
||||
if (this.element.hidden || this.element.hasAttribute("zen-hiding")) {
|
||||
return;
|
||||
}
|
||||
this.element.setAttribute("zen-hiding", "true");
|
||||
Promise.allSettled(
|
||||
this.element.getAnimations().map(animation => animation.finished)
|
||||
).then(() => {
|
||||
if (!this.element.hasAttribute("zen-hiding")) {
|
||||
return;
|
||||
}
|
||||
this.element.removeAttribute("zen-hiding");
|
||||
if (this.shouldBeVisible) {
|
||||
return;
|
||||
}
|
||||
this.element.hidden = true;
|
||||
this.manager.onCardVisibilityChanged();
|
||||
});
|
||||
}
|
||||
|
||||
// Mirrors the original bar behavior: hide instantly when the card's own
|
||||
@@ -412,7 +438,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);
|
||||
}
|
||||
}
|
||||
@@ -451,9 +495,28 @@ class nsZenMediaController {
|
||||
window.addEventListener("TabBrowserDiscarded", onTabDiscardedOrClosed);
|
||||
|
||||
window.addEventListener("DOMAudioPlaybackStarted", event => {
|
||||
const browser = event.target;
|
||||
// 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 card = this.#cardForBrowser(browser);
|
||||
if (!card || card.isSharing) {
|
||||
return;
|
||||
}
|
||||
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(
|
||||
event.target.browsingContext.mediaController,
|
||||
event.target
|
||||
browser.browsingContext.mediaController,
|
||||
browser
|
||||
);
|
||||
});
|
||||
|
||||
@@ -483,8 +546,23 @@ class nsZenMediaController {
|
||||
);
|
||||
}
|
||||
|
||||
#cardForBrowser(browser) {
|
||||
return (
|
||||
Array.from(this.#cards.values()).find(
|
||||
card => card.browser.browserId === browser.browserId
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
activateMediaControls(mediaController, browser) {
|
||||
if (!mediaController?.isActive || this.#cards.has(mediaController.id)) {
|
||||
if (
|
||||
!mediaController?.isActive ||
|
||||
this.#cards.has(mediaController.id) ||
|
||||
// Never stack more than one card for the same browser: sites that
|
||||
// activate a fresh controller for every sound they play (e.g.
|
||||
// notification pings) keep their one card.
|
||||
this.#cardForBrowser(browser)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -570,9 +648,24 @@ class nsZenMediaController {
|
||||
|
||||
#addCard(key, browser, controller = null) {
|
||||
const element = this.#createCardElement();
|
||||
const card = new ZenMediaCard(this, element, browser, controller);
|
||||
let card;
|
||||
try {
|
||||
card = new ZenMediaCard(this, element, browser, controller);
|
||||
} catch (e) {
|
||||
console.error("Failed to create media card", e);
|
||||
// The controller went away while the card was being set up.
|
||||
element.remove();
|
||||
return null;
|
||||
}
|
||||
this.#cards.set(key, card);
|
||||
card.refreshVisibility();
|
||||
if (controller) {
|
||||
// Media cards start hidden and appear via the delayed check in
|
||||
// DOMAudioPlaybackStarted or on tab switch, like the original bar;
|
||||
// sharing cards show right away.
|
||||
card.element.hidden = true;
|
||||
} else {
|
||||
card.refreshVisibility();
|
||||
}
|
||||
this.onCardVisibilityChanged();
|
||||
return card;
|
||||
}
|
||||
@@ -589,18 +682,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);
|
||||
|
||||
@@ -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], [zen-hiding]) {
|
||||
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,47 @@
|
||||
)
|
||||
)
|
||||
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;
|
||||
}
|
||||
|
||||
/* Removed and hiding cards sink down behind the stack while fading out */
|
||||
&[zen-removing],
|
||||
&[zen-hiding] {
|
||||
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);
|
||||
}
|
||||
|
||||
/* Removed cards additionally leave the flex flow instantly (bottom is
|
||||
set inline to where the card was), so only translate/opacity/filter
|
||||
animate; hiding cards stay in flow until the animation finishes. */
|
||||
&[zen-removing] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&[stack-overflow] {
|
||||
display: none;
|
||||
}
|
||||
@@ -249,7 +285,7 @@
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: height 0.2s ease-out;
|
||||
transition: height 0.3s var(--zen-media-stack-easing);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -649,6 +649,7 @@ class nsZenWorkspaces {
|
||||
if (Math.abs(delta) < scrollThreshold) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
// Determine scroll direction
|
||||
let rawDirection = delta > 0 ? 1 : -1;
|
||||
|
||||
@@ -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"]);
|
||||
@@ -54,33 +58,6 @@ const kInteractiveRoles = new Set([
|
||||
"treegrid",
|
||||
]);
|
||||
|
||||
// Cursors that signal the page considers the area interactive or draggable.
|
||||
const kInteractiveCursors = new Set([
|
||||
"pointer",
|
||||
"grab",
|
||||
"grabbing",
|
||||
"move",
|
||||
"all-scroll",
|
||||
"text",
|
||||
"vertical-text",
|
||||
"cell",
|
||||
"crosshair",
|
||||
"col-resize",
|
||||
"row-resize",
|
||||
"n-resize",
|
||||
"e-resize",
|
||||
"s-resize",
|
||||
"w-resize",
|
||||
"ne-resize",
|
||||
"nw-resize",
|
||||
"se-resize",
|
||||
"sw-resize",
|
||||
"ew-resize",
|
||||
"ns-resize",
|
||||
"nesw-resize",
|
||||
"nwse-resize",
|
||||
]);
|
||||
|
||||
const kGestureListenerOptions = { mozSystemGroup: true, capture: true };
|
||||
|
||||
const kGestureEvents = ["mousemove", "mouseup", "dragstart", "unload"];
|
||||
@@ -90,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.
|
||||
@@ -172,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) {
|
||||
@@ -191,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,
|
||||
@@ -276,7 +269,12 @@ export class ZenWindowDragChild extends JSWindowActorChild {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this.#hasInteractiveCursor(target);
|
||||
// The effective cursor, resolved the same way it is shown to the user.
|
||||
return lazy.zenWindowDragUtils.isInteractiveCursor(
|
||||
event.composedTarget,
|
||||
event.clientX,
|
||||
event.clientY
|
||||
);
|
||||
}
|
||||
|
||||
#isSelectableText(node) {
|
||||
@@ -307,15 +305,4 @@ export class ZenWindowDragChild extends JSWindowActorChild {
|
||||
const role = element.getAttribute?.("role");
|
||||
return !!role && kInteractiveRoles.has(role.toLowerCase());
|
||||
}
|
||||
|
||||
#hasInteractiveCursor(element) {
|
||||
const style = element.ownerGlobal?.getComputedStyle(element);
|
||||
if (!style) {
|
||||
return false;
|
||||
}
|
||||
// The keyword is always the last component of the computed value.
|
||||
// Don't split on "," as url() cursor images may contain commas.
|
||||
const cursor = style.cursor.match(/[a-z-]+$/)?.[0];
|
||||
return kInteractiveCursors.has(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,15 @@ interface nsIZenWindowDragUtils : nsISupports {
|
||||
* @param node The node to check.
|
||||
*/
|
||||
boolean isInteractiveContent(in Node node);
|
||||
|
||||
/**
|
||||
* @brief Whether the effective cursor for the given node signals
|
||||
* interactive or draggable content (pointer, text, move, resize, ...).
|
||||
* Unlike computed style, this resolves cursor: auto the same way the
|
||||
* cursor actually shown to the user is chosen.
|
||||
* @param node The deepest node hit by the event.
|
||||
* @param clientX Viewport X coordinate of the event, in CSS pixels.
|
||||
* @param clientY Viewport Y coordinate of the event, in CSS pixels.
|
||||
*/
|
||||
boolean isInteractiveCursor(in Node node, in float clientX, in float clientY);
|
||||
};
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
|
||||
#include "nsZenWindowDragUtils.h"
|
||||
|
||||
#include "Units.h"
|
||||
#include "mozilla/PresShell.h"
|
||||
#include "mozilla/ServoStyleConsts.h"
|
||||
#include "mozilla/dom/Element.h"
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsIContent.h"
|
||||
#include "nsIFrame.h"
|
||||
|
||||
namespace zen {
|
||||
|
||||
@@ -30,4 +34,57 @@ nsZenWindowDragUtils::IsInteractiveContent(nsINode* aNode, bool* aResult) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsZenWindowDragUtils::IsInteractiveCursor(nsINode* aNode, float aClientX,
|
||||
float aClientY, bool* aResult) {
|
||||
using mozilla::StyleCursorKind;
|
||||
|
||||
*aResult = false;
|
||||
NS_ENSURE_ARG_POINTER(aNode);
|
||||
|
||||
nsIContent* content = nsIContent::FromNode(aNode);
|
||||
nsIFrame* frame = content ? content->GetPrimaryFrame() : nullptr;
|
||||
if (!frame) {
|
||||
return NS_OK;
|
||||
}
|
||||
nsIFrame* rootFrame = frame->PresShell()->GetRootFrame();
|
||||
if (!rootFrame) {
|
||||
return NS_OK;
|
||||
}
|
||||
nsPoint point(mozilla::CSSPixel::ToAppUnits(aClientX),
|
||||
mozilla::CSSPixel::ToAppUnits(aClientY));
|
||||
point -= frame->GetOffsetTo(rootFrame);
|
||||
|
||||
switch (frame->GetCursor(point).mCursor) {
|
||||
case StyleCursorKind::Pointer:
|
||||
case StyleCursorKind::Cell:
|
||||
case StyleCursorKind::Crosshair:
|
||||
case StyleCursorKind::Text:
|
||||
case StyleCursorKind::VerticalText:
|
||||
case StyleCursorKind::Move:
|
||||
case StyleCursorKind::Grab:
|
||||
case StyleCursorKind::Grabbing:
|
||||
case StyleCursorKind::EResize:
|
||||
case StyleCursorKind::NResize:
|
||||
case StyleCursorKind::NeResize:
|
||||
case StyleCursorKind::NwResize:
|
||||
case StyleCursorKind::SResize:
|
||||
case StyleCursorKind::SeResize:
|
||||
case StyleCursorKind::SwResize:
|
||||
case StyleCursorKind::WResize:
|
||||
case StyleCursorKind::EwResize:
|
||||
case StyleCursorKind::NsResize:
|
||||
case StyleCursorKind::NeswResize:
|
||||
case StyleCursorKind::NwseResize:
|
||||
case StyleCursorKind::ColResize:
|
||||
case StyleCursorKind::RowResize:
|
||||
case StyleCursorKind::AllScroll:
|
||||
*aResult = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
} // namespace zen
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"brandShortName": "Zen",
|
||||
"brandFullName": "Zen Browser",
|
||||
"release": {
|
||||
"displayVersion": "1.21.11b",
|
||||
"displayVersion": "1.21.13b",
|
||||
"github": {
|
||||
"repo": "zen-browser/desktop"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user