Compare commits

..

6 Commits

Author SHA1 Message Date
mr. m
49bf2eb60a no-bug: Remove patches 2026-08-06 20:18:28 +02:00
mr. m
f669db586b Merge branch 'dev' into feat/9324 2026-08-06 15:05:20 +02:00
mr. m
94b2a4b67e no-bug: Add border render support 2026-06-20 16:03:48 +02:00
mr. m
cf98127a5e Merge branch 'dev' into feat/9324
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-06-19 08:29:45 +02:00
mr. m
b7a8e79299 no-bug: Apply squircles to elements 2026-06-06 14:08:36 +02:00
mr. m
0048f21a52 no-bug: Squircles support 2026-06-04 01:37:46 +02:00
27 changed files with 100 additions and 621 deletions

View File

@@ -48,11 +48,6 @@ 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 }

View File

@@ -95,6 +95,9 @@
- name: browser.search.widget.new
value: true
- name: layout.css.corner-shape.enabled
value: true
# Disabled from https://searchfox.org/firefox-main/rev/d6bfff43852356ca98af848b4705d37f8d41856f/modules/libpref/init/StaticPrefList.yaml#2008
# Only enabled for windows, doesn't really fit inside Zen.
- name: browser.startup.preXulSkeletonUI

View File

@@ -50,7 +50,7 @@
value: true
- name: zen.view.drag-window-from-content.height-percentage
value: 5
value: 10
- name: zen.view.borderless-fullscreen
value: true

View File

@@ -131,10 +131,10 @@
</box>
<html:input type="range" value="0.4" step="0.001" id="PanelUI-zen-gradient-generator-opacity"
#ifdef XP_MACOSX
max="0.9"
max="0.8"
min="0.30"
#else
max="0.9"
max="0.8"
min="0.25"
#endif
/>

View File

@@ -1184,11 +1184,6 @@ Preferences.addAll([
type: "bool",
default: true,
},
{
id: "zen.view.drag-window-from-content",
type: "bool",
default: true,
},
{
id: "zen.urlbar.behavior",
type: "string",

View File

@@ -61,15 +61,6 @@
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"

View File

@@ -1,136 +0,0 @@
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

View File

@@ -1,18 +0,0 @@
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);
}

View File

@@ -1,72 +0,0 @@
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) {

View File

@@ -39,14 +39,5 @@
"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"
}
]

View File

@@ -22,64 +22,27 @@ class ZenSidebarNotification extends MozLitElement {
static properties = {
headingL10nId: { type: String, fluent: true },
links: { type: Array },
autoHideMs: { type: Number },
};
#autoHideAnimation = null;
constructor({ headingL10nId = "", links = [], autoHideMs = 0 } = {}) {
constructor({ headingL10nId = "", links = [] } = {}) {
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().then(() => this.#startAutoHide());
this.#animateIn();
}
}
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
@@ -99,15 +62,6 @@ 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`

View File

@@ -7,7 +7,6 @@ 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;
@@ -26,7 +25,6 @@ export default function checkForZenUpdates() {
);
createSidebarNotification({
headingL10nId: "zen-sidebar-notification-updated-heading",
autoHideMs: ZEN_UPDATE_NOTIFICATION_TIMEOUT_MS,
links: [
{
url: Services.urlFormatter.formatURL(

View File

@@ -77,18 +77,6 @@
}
}
.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;

View File

@@ -185,7 +185,7 @@
.toolbarbutton-1:not(#tabs-newtab-button),
.urlbar-page-action,
.identity-box-button {
--tab-border-radius: 6px;
--tab-border-radius: 8px;
--toolbarbutton-border-radius: var(--tab-border-radius);
--toolbarbutton-padding-inner: 6px;
--toolbarbutton-padding-outer: 1px;

View File

@@ -213,6 +213,8 @@
--toolbarbutton-border-radius: 6px;
--urlbar-margin-inline: 1px !important;
--zen-squircle-value: 1.4;
--tab-icon-overlay-stroke: light-dark(white, black) !important;
--tab-close-button-padding: 4px !important;
@@ -262,7 +264,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.1));
--zen-sidebar-notification-outline: 1px solid light-dark(transparent, rgba(255, 255, 255, 0.15));
--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));
@@ -344,3 +346,9 @@
}
%include zen-buttons.css
@media (-moz-pref('layout.css.corner-shape.enabled')) {
*:not(.no-squircles) {
corner-shape: superellipse(var(--zen-squircle-value));
}
}

View File

@@ -41,11 +41,11 @@
&:hover {
background: light-dark(rgb(41, 41, 41), rgb(204, 204, 204));
scale: 1.05;
scale: 1.02;
}
&:hover:active {
scale: 0.95;
scale: 0.98;
}
& label {

View File

@@ -4,8 +4,8 @@
<html:template id="zen-glance-sidebar-template">
<vbox class="zen-glance-sidebar-container">
<toolbarbutton class="zen-glance-sidebar-close toolbarbutton-1" command="cmd_zenGlanceClose" data-l10n-id="zen-general-confirm" />
<toolbarbutton class="zen-glance-sidebar-open toolbarbutton-1" command="cmd_zenGlanceExpand" />
<toolbarbutton class="zen-glance-sidebar-split toolbarbutton-1" command="cmd_zenGlanceSplit" />
<toolbarbutton class="no-squircles zen-glance-sidebar-close toolbarbutton-1" command="cmd_zenGlanceClose" data-l10n-id="zen-general-confirm" />
<toolbarbutton class="no-squircles zen-glance-sidebar-open toolbarbutton-1" command="cmd_zenGlanceExpand" />
<toolbarbutton class="no-squircles zen-glance-sidebar-split toolbarbutton-1" command="cmd_zenGlanceSplit" />
</vbox>
</html:template>

View File

@@ -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 = 3;
const MAX_STACKED_CARDS = 5;
// 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,10 +46,6 @@ 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),
@@ -66,7 +62,7 @@ class ZenMediaCard {
this.element.classList.toggle("playing", controller.isPlaying);
this.updateMetadata();
this.updatePositionState(positionState);
this.updatePositionState(controller.getPositionState());
this.updateSupportedKeys();
} else {
this.element.setAttribute("media-sharing", "");
@@ -168,33 +164,11 @@ class ZenMediaCard {
}
setHidden(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();
}
if (this.element.hidden === hidden) {
return;
}
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();
});
this.element.hidden = hidden;
this.manager.onCardVisibilityChanged();
}
// Mirrors the original bar behavior: hide instantly when the card's own
@@ -438,25 +412,7 @@ class ZenMediaCard {
this.#tabTimeout = null;
}
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.element.remove();
this.manager.onCardDestroyed(this);
}
}
@@ -495,28 +451,9 @@ 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(
browser.browsingContext.mediaController,
browser
event.target.browsingContext.mediaController,
event.target
);
});
@@ -546,23 +483,8 @@ 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) ||
// 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)
) {
if (!mediaController?.isActive || this.#cards.has(mediaController.id)) {
return;
}
@@ -648,24 +570,9 @@ class nsZenMediaController {
#addCard(key, browser, controller = null) {
const element = this.#createCardElement();
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;
}
const card = new ZenMediaCard(this, element, browser, controller);
this.#cards.set(key, card);
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();
}
card.refreshVisibility();
this.onCardVisibilityChanged();
return card;
}
@@ -682,31 +589,18 @@ 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);

View File

@@ -9,7 +9,6 @@
--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;
@@ -89,7 +88,7 @@
animation: zen-back-and-forth-text 10s infinite ease-in-out;
}
& .zen-media-card:not([zen-removing], [zen-hiding]) {
& .zen-media-card {
transform: none;
opacity: 1;
}
@@ -115,10 +114,10 @@
padding: 0 6px;
pointer-events: none;
transition:
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);
max-height 0.2s ease,
opacity 0.2s ease,
transform 0.2s ease,
padding 0.2s ease;
}
.zen-media-current-time,
@@ -150,47 +149,12 @@
)
)
scale(calc(1 - 0.04 * var(--zen-media-card-peek-level, 0)));
opacity: calc(1 - 0.3 * var(--zen-media-card-peek-level, 0));
filter: blur(0);
opacity: calc(1 - 0.4 * var(--zen-media-card-peek-level, 0));
transition:
transform 0.35s var(--zen-media-stack-easing),
opacity 0.3s var(--zen-media-stack-easing),
filter 0.3s var(--zen-media-stack-easing),
transform 0.1s ease-out,
opacity 0.2s ease-out,
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;
}
@@ -285,7 +249,7 @@
overflow: visible;
position: relative;
z-index: 2;
transition: height 0.3s var(--zen-media-stack-easing);
transition: height 0.2s ease-out;
}
}

View File

@@ -649,7 +649,6 @@ class nsZenWorkspaces {
if (Math.abs(delta) < scrollThreshold) {
return;
}
event.preventDefault();
// Determine scroll direction
let rawDirection = delta > 0 ? 1 : -1;

View File

@@ -31,6 +31,7 @@
& toolbarbutton {
margin: 0;
max-width: 28px;
border-radius: var(--toolbarbutton-border-radius);
height: 28px;
display: flex;
justify-content: center;

View File

@@ -235,7 +235,7 @@
}
@media (-moz-platform: macos) {
--border-radius-medium: 12px;
--border-radius-medium: 14px;
--tab-border-radius: 8px;
}
@@ -1228,6 +1228,8 @@
background: var(--zen-essential-tab-selected-bg);
margin: var(--zen-essential-bg-margin);
border-radius: calc(var(--border-radius-medium) - var(--zen-essential-bg-margin));
/* stylelint-disable-next-line property-no-unknown */
corner-shape: var(--zen-squircle-value);
position: absolute;
inset: 0;
z-index: 0;

View File

@@ -26,10 +26,6 @@ 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"]);
@@ -58,6 +54,33 @@ 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"];
@@ -67,9 +90,6 @@ 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.
@@ -152,18 +172,8 @@ 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) {
@@ -181,12 +191,9 @@ export class ZenWindowDragChild extends JSWindowActorChild {
event.preventDefault();
return;
}
if (!this.#dragThresholdPx) {
return;
}
const point = this.#screenPoint(event);
const threshold =
this.#dragThresholdPx * this.contentWindow.devicePixelRatio;
DRAG_START_THRESHOLD_PX * this.contentWindow.devicePixelRatio;
if (
Math.hypot(
point.screenX - this.#startScreenX,
@@ -269,12 +276,7 @@ export class ZenWindowDragChild extends JSWindowActorChild {
return true;
}
}
// The effective cursor, resolved the same way it is shown to the user.
return lazy.zenWindowDragUtils.isInteractiveCursor(
event.composedTarget,
event.clientX,
event.clientY
);
return this.#hasInteractiveCursor(target);
}
#isSelectableText(node) {
@@ -305,4 +307,15 @@ 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);
}
}

View File

@@ -15,42 +15,19 @@ XPCOMUtils.defineLazyServiceGetter(
export class ZenWindowDragParent extends JSWindowActorParent {
receiveMessage(message) {
if (message.name !== "ZenWindowDrag:StartDrag") {
return;
}
const win = this.browsingContext.topChromeWindow;
if (!win || win.closed) {
return undefined;
if (!win || win.closed || win.windowState === win.STATE_FULLSCREEN) {
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);
}
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;
}
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")
);
lazy.zenDragAndDropService.beginNativeWindowMove(win);
}
}

View File

@@ -20,15 +20,4 @@ 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);
};

View File

@@ -4,13 +4,9 @@
#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 {
@@ -34,57 +30,4 @@ 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

View File

@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.21.13b",
"displayVersion": "1.21.11b",
"github": {
"repo": "zen-browser/desktop"
},