Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6f31508
no-bug: Keep sidebar controls stable during compact-mode transitions
yosofbadr Oct 6, 2026
b4e1605
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 8, 2026
216d99d
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 9, 2026
cb0616e
Merge dev into fix/sidebar-animation and preserve layout events
yosofbadr Oct 11, 2026
395fce8
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 11, 2026
f44c1c1
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 12, 2026
515d001
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 12, 2026
3bac885
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 13, 2026
2060335
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 13, 2026
3632f56
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 15, 2026
df22b27
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 15, 2026
45240f4
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 16, 2026
05e7c2d
gh-15916: Keep sidebar controls stable with a right sidebar
yosofbadr Oct 16, 2026
3ae6d21
gh-15916: Smooth right-sidebar toolbar transitions
yosofbadr Oct 16, 2026
b62f534
gh-15916: Preserve the original toolbar button order
yosofbadr Oct 16, 2026
21846b7
gh-15916: Trim redundant transition comments
yosofbadr Oct 16, 2026
cb38b6a
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 17, 2026
2cf99a4
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 18, 2026
3820810
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Oct 22, 2026
b7aed0d
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Nov 1, 2026
d7f0588
Merge branch 'dev' into fix/sidebar-animation
yosofbadr Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 238 additions & 35 deletions src/zen/compact-mode/ZenCompactMode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,211 @@ window.gZenCompactModeManager = {
window.dispatchEvent(new CustomEvent("ZenCompactMode:LayoutUpdate"));
},

get _canAnimateSidebar() {
return (
lazy.COMPACT_MODE_CAN_ANIMATE_SIDEBAR &&
!this.isSidebarPotentiallyOpen() &&
!this._preventAnimateCollapse &&
typeof this._wasInCompactMode === "undefined"
);
},

_prepareToolbarTransition() {
if (
AppConstants.platform !== "macosx" ||
document.documentElement.matches(":dir(rtl)") ||
gZenVerticalTabsManager._hasSetSingleToolbar ||
!this.canHideSidebar ||
this.canHideToolbar ||
!this._canAnimateSidebar
) {
return null;
}
const controls = [
gZenVerticalTabsManager.actualWindowButtons,
document.getElementById("zen-sidebar-top-buttons-customization-target"),
];
const titlebar = document.getElementById("titlebar");
const navbar = document.getElementById("nav-bar");
const navbarContents = document.getElementById(
"nav-bar-customization-target"
);
const header = document.getElementById("zen-sidebar-top-buttons");
const styles = [
...controls.map(element => [element, "transform"]),
[titlebar, "margin-top"],
[navbarContents, "margin-left"],
[navbar, "margin-left"],
[navbar, "margin-right"],
[header, "margin-left"],
].map(([element, property]) => ({
element,
property,
value: element.style.getPropertyValue(property),
priority: element.style.getPropertyPriority(property),
}));
const bounds = controls.map(element => element.getBoundingClientRect());
const hiding = !this.preference;
const initialNavbarX = navbarContents.getBoundingClientRect().x;
const sidebarExpanded = gZenVerticalTabsManager._prefsSidebarExpanded;
const rightSide = this.sidebarIsOnRight;
const sidebar = this.sidebar;
const animations = [];
// Reserve the header's height so the tab list does not jump.
titlebar.style.marginTop = `${
navbar.getBoundingClientRect().bottom -
document.getElementById("browser").getBoundingClientRect().y
}px`;
return {
start(width, options) {
if (rightSide) {
// Temporarily use idle CSS to measure the controls' final positions.
sidebar.removeAttribute("animate");
document.documentElement.removeAttribute("zen-compact-animating");
const destination = controls.map(element =>
element.getBoundingClientRect()
);
const idleInset = getComputedStyle(navbar).marginLeft;
document.documentElement.setAttribute(
"zen-compact-animating",
"true"
);
sidebar.setAttribute("animate", "true");
const animatingInset = getComputedStyle(navbar).marginLeft;
const headerWidth = sidebarExpanded
? header.getBoundingClientRect().width
: width;
const layout = [
{
element: navbar,
property: "marginLeft",
values: [animatingInset, idleInset],
},
{
element: navbar,
property: "marginRight",
values: [sidebarExpanded ? "0px" : `-${width}px`, "0px"],
},
{
element: header,
property: "marginLeft",
values: [sidebarExpanded ? `-${headerWidth}px` : "0px", "0px"],
},
];
if (!hiding) {
layout.forEach(({ values }) => values.reverse());
}
const applyLayout = index =>
layout.forEach(({ element, property, values }) => {
element.style[property] = values[index];
});
applyLayout(1);
const shifted = controls.map(element =>
element.getBoundingClientRect()
);
applyLayout(0);
if (!hiding) {
controls.forEach((element, index) => {
if (index === 0 || sidebarExpanded) {
destination[index] = DOMRect.fromRect(shifted[index]);
}
});
if (sidebarExpanded) {
// The expanded header finishes in the sidebar, not the navbar.
destination[1].x =
sidebar.getBoundingClientRect().x +
parseFloat(getComputedStyle(sidebar).paddingLeft) -
width;
}
}
const sidebarDisplacement = hiding ? -width : width;
controls.forEach((element, index) => {
const current = element.getBoundingClientRect();
const x = bounds[index].x - current.x;
const y = bounds[index].y - current.y;
const from = `translate(${x}px, ${y}px)`;
const displacement = index === 1 ? sidebarDisplacement : 0;
const destinationX =
destination[index].x - shifted[index].x + displacement;
const destinationY = destination[index].y - shifted[index].y;
const to = `translate(${destinationX}px, ${destinationY}px)`;
element.style.transform = from;
animations.push(
gZenUIManager.motion.animate(
element,
{ transform: [from, to] },
options
)
);
});
for (const { element, property, values } of layout) {
animations.push(
gZenUIManager.motion.animate(
element,
{ [property]: values },
options
)
);
}
return;
}
const leadingSpace =
navbarContents.getBoundingClientRect().x -
navbar.getBoundingClientRect().x;
const compactMargin =
bounds[1].x +
controls[1].getBoundingClientRect().width -
navbarContents.getBoundingClientRect().x +
(hiding ? width : 0);
const margins = hiding
? [
initialNavbarX - navbarContents.getBoundingClientRect().x,
compactMargin,
]
: [
compactMargin,
sidebarExpanded ? -leadingSpace : compactMargin - width,
];
navbarContents.style.marginLeft = `${margins[0]}px`;
animations.push(
gZenUIManager.motion.animate(
navbarContents,
{ marginLeft: margins },
options
)
);
controls.forEach((element, index) => {
const current = element.getBoundingClientRect();
const x = bounds[index].x - current.x;
const y = bounds[index].y - current.y;
const from = `translate(${x}px, ${y}px)`;
const to = `translate(${x + (hiding ? width : -width)}px, ${y}px)`;
element.style.transform = from;
animations.push(
gZenUIManager.motion.animate(
element,
{ transform: [from, to] },
options
)
);
});
},
restore() {
animations.forEach(animation => animation.cancel());
for (const { element, property, value, priority } of styles) {
if (value) {
element.style.setProperty(property, value, priority);
} else {
element.style.removeProperty(property);
}
}
},
};
},

async _updateEvent() {
const value = !this.preference;
const toolbarTransition = this._prepareToolbarTransition();
const setAttrs = () => {
// We use this element in order to make it persis across restarts, by using the XULStore.
// main-window can't store attributes other than window sizes, so we use this instead
Expand All @@ -386,17 +589,21 @@ window.gZenCompactModeManager = {
// IF we are animating IN, call the callbacks first so we can calculate the width
// once the window buttons are shown
gZenWorkspaces._processingResize = true;
if (!this.preference) {
setAttrs();
this._dispatchLayoutUpdate();
await this.animateCompactMode(value);
} else {
await this.animateCompactMode(value);
setAttrs();
this._dispatchLayoutUpdate();
try {
if (!this.preference) {
setAttrs();
this._dispatchLayoutUpdate();
await this.animateCompactMode(value, toolbarTransition);
} else {
await this.animateCompactMode(value, toolbarTransition);
setAttrs();
this._dispatchLayoutUpdate();
}
} finally {
toolbarTransition?.restore();
gZenWorkspaces._processingResize = false;
}
this.updateContextMenu();
gZenWorkspaces._processingResize = false;
if (isUrlbarFocused) {
gURLBar.focus();
}
Expand Down Expand Up @@ -424,19 +631,13 @@ window.gZenCompactModeManager = {
);
},

animateCompactMode(isCompactMode) {
animateCompactMode(isCompactMode, toolbarTransition = null) {
return new Promise(resolve => {
document.documentElement.setAttribute("zen-compact-animating", "true");
const canHideSidebar = this.canHideSidebar;
let canAnimate =
lazy.COMPACT_MODE_CAN_ANIMATE_SIDEBAR &&
!this.isSidebarPotentiallyOpen() &&
!this._preventAnimateCollapse;
const canAnimate = this._canAnimateSidebar;
delete this._preventAnimateCollapse;
if (typeof this._wasInCompactMode !== "undefined") {
canAnimate = false;
delete this._wasInCompactMode;
}
delete this._wasInCompactMode;
// Do this so we can get the correct width ONCE compact mode styled have been applied
if (canAnimate) {
this.sidebar.setAttribute("animate", "true");
Expand All @@ -449,9 +650,10 @@ window.gZenCompactModeManager = {
this.sidebar.style.removeProperty("margin-right");
this.sidebar.style.removeProperty("margin-left");
this.sidebar.style.removeProperty("transform");
let sidebarWidth = window.windowUtils.getBoundsWithoutFlushing(
this.sidebar
).width;
// Reparenting controls invalidates layout; compensation needs fresh bounds.
let sidebarWidth = toolbarTransition
? this.sidebar.getBoundingClientRect().width
: window.windowUtils.getBoundsWithoutFlushing(this.sidebar).width;
const elementSeparation = ZenThemeModifier.elementSeparation;
if (!canAnimate) {
this.sidebar.removeAttribute("animate");
Expand All @@ -465,8 +667,16 @@ window.gZenCompactModeManager = {
resolve();
return;
}
sidebarWidth -= elementSeparation * (isCompactMode ? 1 : 3);
sidebarWidth -=
elementSeparation * (toolbarTransition || isCompactMode ? 1 : 3);
const animationOptions = {
ease: isCompactMode ? "easeIn" : "easeOut",
type: "spring",
bounce: 0,
duration: 0.1,
};
if (canHideSidebar && isCompactMode) {
toolbarTransition?.start(sidebarWidth, animationOptions);
this._setElementExpandAttribute(this.sidebar, false);
gZenUIManager.motion
.animate(
Expand All @@ -478,12 +688,7 @@ window.gZenCompactModeManager = {
],
marginLeft: [0, this.sidebarIsOnRight ? 0 : `-${sidebarWidth}px`],
},
{
ease: "easeIn",
type: "spring",
bounce: 0,
duration: 0.1,
}
animationOptions
)
.then(() => {
this.sidebar.style.transition = "none";
Expand Down Expand Up @@ -517,21 +722,19 @@ window.gZenCompactModeManager = {
} else {
this.sidebar.style.marginLeft = `-${sidebarWidth}px`;
}
toolbarTransition?.start(sidebarWidth, animationOptions);
gZenUIManager.motion
.animate(
this.sidebar,
this.sidebarIsOnRight
? {
marginRight: [`-${sidebarWidth}px`, 0],
transform: ["translateX(100%)", "translateX(0)"],
...(!toolbarTransition && {
transform: ["translateX(100%)", "translateX(0)"],
}),
}
: { marginLeft: [`-${sidebarWidth}px`, 0] },
{
ease: "easeOut",
type: "spring",
bounce: 0,
duration: 0.1,
}
animationOptions
)
.then(() => {
this.sidebar.removeAttribute("animate");
Expand Down
18 changes: 18 additions & 0 deletions src/zen/compact-mode/sidebar.inc.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@
) !important;
}

@media (-moz-platform: macos) and (not -moz-pref('zen.view.compact.hide-toolbar')) {
&:dir(ltr):not([zen-single-toolbar='true'])[zen-right-side='true'] #nav-bar #zen-sidebar-top-buttons-customization-target {
padding-inline-start: 0 !important;
}

&:dir(ltr):not([zen-single-toolbar='true']):not([zen-right-side='true']) #nav-bar {
& > .titlebar-buttonbox-container {
margin-inline-start: calc(
var(--zen-toolbox-padding) - (var(--zen-element-separation) - 3px)
);
}

& #zen-sidebar-top-buttons-customization-target {
padding-inline-start: 0 !important;
}
}
}

&[zen-window-buttons-reversed='true'] #zen-appcontent-navbar-wrapper #nav-bar {
margin-right: var(--zen-element-separation) !important;
margin-left: calc(var(--zen-element-separation) - 3px) !important;
Expand Down
3 changes: 3 additions & 0 deletions src/zen/tests/compact_mode/browser.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

[DEFAULT]

["browser_compact_mode_controls.js"]
skip-if = ["os != 'mac'"]

["browser_compact_mode_layout_event.js"]

["browser_compact_mode_width.js"]
Expand Down
Loading