1
0
Fork 1
mirror of https://github.com/zen-browser/desktop.git synced 2025-07-07 21:49:58 +02:00

chore: Continued working on containerized essentials, b=(no-bug), c=tabs, workspaces

This commit is contained in:
Mr. M 2025-04-22 00:33:15 +02:00
parent 8136387a75
commit 47fbae7e0d
No known key found for this signature in database
GPG key ID: 6292C4C8F8652B18
3 changed files with 456 additions and 28 deletions

View file

@ -49,3 +49,393 @@ interface nsIXPCComponents extends nsISupports {
readonly Constructor: (aClass: any, aIID: any, aFlags: any) => any;
returnCode: any;
}
/**
* TS-TODO - Needs typing.
*
* This file contains type stubs for loading things from Gecko. All of these
* types should be used in the correct places eventually.
*/
/**
* Namespace anything that has its types mocked out here. These definitions are
* only "good enough" to get the type checking to pass in this directory.
* Eventually some more structured solution should be found. This namespace is
* global and makes sure that all the definitions inside do not clash with
* naming.
*/
declare namespace MockedExports {
/**
* This interface teaches ChromeUtils.importESModule how to find modules.
*/
interface KnownModules {
Services: typeof import('Services');
'resource://gre/modules/AppConstants.sys.mjs': typeof import('resource://gre/modules/AppConstants.sys.mjs');
'resource:///modules/CustomizableUI.sys.mjs': typeof import('resource:///modules/CustomizableUI.sys.mjs');
'resource:///modules/CustomizableWidgets.sys.mjs': typeof import('resource:///modules/CustomizableWidgets.sys.mjs');
}
interface ChromeUtils {
/**
* This function reads the KnownModules and resolves which import to use.
* If you are getting the TS2345 error:
*
* Argument of type '"resource:///.../file.sys.mjs"' is not assignable to
* parameter of type
*
* Then add the file path to the KnownModules above.
*/
importESModule: <S extends keyof KnownModules>(module: S) => KnownModules[S];
defineESModuleGetters: (target: any, mappings: any) => void;
}
interface MessageManager {
loadFrameScript(url: string, flag: boolean): void;
sendAsyncMessage: (event: string, data: any) => void;
addMessageListener: (event: string, listener: (event: any) => void) => void;
}
// This is the thing in window.gBrowser, defined in
// https://searchfox.org/mozilla-central/source/browser/base/content/tabbrowser.js
interface Browser {
addWebTab: (url: string, options: any) => BrowserTab;
contentPrincipal: any;
selectedTab: BrowserTab;
selectedBrowser?: ChromeBrowser;
messageManager: MessageManager;
ownerDocument?: ChromeDocument;
tabs: BrowserTab[];
}
interface BrowserGroup {
readonly tabs: BrowserTab[];
readonly group?: BrowserGroup;
}
// This is a tab in a browser, defined in
// https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/base/content/tabbrowser.js#2580
interface BrowserTab extends XULElement {
linkedBrowser: ChromeBrowser;
readonly group?: BrowserGroup;
}
interface BrowserWindow extends Window {
gBrowser: Browser;
focus(): void;
}
// The thing created in https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/base/content/tabbrowser.js#2088
// This is linked to BrowserTab.
interface ChromeBrowser {
browsingContext?: BrowsingContext;
browserId: number;
}
interface BrowsingContext {
/**
* A unique identifier for the browser element that is hosting this
* BrowsingContext tree. Every BrowsingContext in the element's tree will
* return the same ID in all processes and it will remain stable regardless of
* process changes. When a browser element's frameloader is switched to
* another browser element this ID will remain the same but hosted under the
* under the new browser element.
* We are using this identifier for getting the active tab ID and passing to
* the profiler back-end. See `getActiveBrowserID` for the usage.
*/
browserId: number;
}
type GetPref<T> = (prefName: string, defaultValue?: T) => T;
type SetPref<T> = (prefName: string, value?: T) => T;
type nsIPrefBranch = {
clearUserPref: (prefName: string) => void;
getStringPref: GetPref<string>;
setStringPref: SetPref<string>;
getCharPref: GetPref<string>;
setCharPref: SetPref<string>;
getIntPref: GetPref<number>;
setIntPref: SetPref<number>;
getBoolPref: GetPref<boolean>;
setBoolPref: SetPref<boolean>;
addObserver: (aDomain: string, aObserver: PrefObserver, aHoldWeak?: boolean) => void;
removeObserver: (aDomain: string, aObserver: PrefObserver) => void;
};
type PrefObserverFunction = (aSubject: nsIPrefBranch, aTopic: 'nsPref:changed', aData: string) => unknown;
type PrefObserver = PrefObserverFunction | { observe: PrefObserverFunction };
interface nsIURI {}
interface SharedLibrary {
start: number;
end: number;
offset: number;
name: string;
path: string;
debugName: string;
debugPath: string;
breakpadId: string;
arch: string;
}
interface ProfileGenerationAdditionalInformation {
sharedLibraries: SharedLibrary[];
}
interface ProfileAndAdditionalInformation {
profile: ArrayBuffer;
additionalInformation?: ProfileGenerationAdditionalInformation;
}
type Services = {
env: {
set: (name: string, value: string) => void;
get: (name: string) => string;
exists: (name: string) => boolean;
};
prefs: nsIPrefBranch;
profiler: {
StartProfiler: (
entryCount: number,
interval: number,
features: string[],
filters?: string[],
activeTabId?: number,
duration?: number
) => void;
StopProfiler: () => void;
IsPaused: () => boolean;
Pause: () => void;
Resume: () => void;
IsSamplingPaused: () => boolean;
PauseSampling: () => void;
ResumeSampling: () => void;
GetFeatures: () => string[];
getProfileDataAsync: (sinceTime?: number) => Promise<object>;
getProfileDataAsArrayBuffer: (sinceTime?: number) => Promise<ArrayBuffer>;
getProfileDataAsGzippedArrayBuffer: (sinceTime?: number) => Promise<ProfileAndAdditionalInformation>;
IsActive: () => boolean;
sharedLibraries: SharedLibrary[];
};
platform: string;
obs: {
addObserver: (observer: object, type: string) => void;
removeObserver: (observer: object, type: string) => void;
};
wm: {
getMostRecentWindow: (name: string) => BrowserWindow;
getMostRecentNonPBWindow: (name: string) => BrowserWindow;
};
focus: {
activeWindow: BrowserWindow;
};
io: {
newURI(url: string): nsIURI;
};
scriptSecurityManager: any;
startup: {
quit: (optionsBitmask: number) => void;
eForceQuit: number;
eRestart: number;
};
};
const EventEmitter: {
decorate: (target: object) => void;
};
const AppConstantsSYSMJS: {
AppConstants: {
platform: string;
};
};
interface BrowsingContextStub {}
interface PrincipalStub {}
interface WebChannelTarget {
browsingContext: BrowsingContextStub;
browser: Browser;
eventTarget: null;
principal: PrincipalStub;
}
interface FaviconData {
uri: nsIURI;
dataLen: number;
data: number[];
mimeType: string;
size: number;
}
const PlaceUtilsSYSMJS: {
PlacesUtils: {
promiseFaviconData: (pageUrl: string | URL | nsIURI, preferredWidth?: number) => Promise<FaviconData>;
// TS-TODO: Add the rest.
};
};
// TS-TODO
const CustomizableUISYSMJS: any;
const CustomizableWidgetsSYSMJS: any;
const PanelMultiViewSYSMJS: any;
const LoaderESM: {
require: (path: string) => any;
};
const Services: Services;
// This class is needed by the Cc importing mechanism. e.g.
// Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
class nsIFilePicker {}
interface FilePicker {
init: (browsingContext: BrowsingContext, title: string, mode: number) => void;
open: (callback: (rv: number) => unknown) => void;
// The following are enum values.
modeGetFolder: number;
returnOK: number;
file: {
path: string;
};
}
interface Cc {
'@mozilla.org/filepicker;1': {
createInstance(instance: nsIFilePicker): FilePicker;
};
}
interface Ci {
nsIFilePicker: nsIFilePicker;
}
interface Cu {
exportFunction: (fn: Function, scope: object, options?: object) => void;
cloneInto: (value: any, scope: object, options?: object) => void;
isInAutomation: boolean;
}
interface FluentLocalization {
/**
* This function sets the attributes data-l10n-id and possibly data-l10n-args
* on the element.
*/
setAttributes(target: Element, id?: string, args?: Record<string, string>): void;
}
}
interface PathUtilsInterface {
split: (path: string) => string[];
isAbsolute: (path: string) => boolean;
}
declare module 'Services' {
export = MockedExports.Services;
}
declare module 'ChromeUtils' {
export = ChromeUtils;
}
declare var ChromeUtils: MockedExports.ChromeUtils;
declare var PathUtils: PathUtilsInterface;
// These global objects can be used directly in JSM files only.
declare var Cu: MockedExports.Cu;
declare var Cc: MockedExports.Cc;
declare var Ci: MockedExports.Ci;
declare var Services: MockedExports.Services;
/**
* This is a variant on the normal Document, as it contains chrome-specific properties.
*/
declare interface ChromeDocument extends Document {
/**
* Create a XUL element of a specific type. Right now this function
* only refines iframes, but more tags could be added.
*/
createXULElement: ((type: 'iframe') => XULIframeElement) & ((type: string) => XULElement);
/**
* This is a fluent instance connected to this document.
*/
l10n: MockedExports.FluentLocalization;
}
/**
* This is a variant on the HTMLElement, as it contains chrome-specific properties.
*/
declare interface ChromeHTMLElement extends HTMLElement {
ownerDocument: ChromeDocument;
}
declare interface XULIframeElement extends XULElement {
contentWindow: Window;
src: string;
}
// `declare interface Window` is TypeScript way to let us implicitely extend and
// augment the already existing Window interface defined in the TypeScript library.
// This makes it possible to define properties that exist in the window object
// while in a privileged context. We assume that all of the environments we run
// in this project will be pribileged, that's why we take this shortcut of
// globally extending the Window type.
// See the ChromeOnly attributes in https://searchfox.org/mozilla-central/rev/896042a1a71066254ceb5291f016ca3dbca21cb7/dom/webidl/Window.webidl#391
//
// openWebLinkIn and openTrustedLinkIn aren't in all privileged windows, but
// they're also defined in the privileged environments we're dealing with in
// this project, so they're defined here for convenience.
declare interface Window {
browsingContext: MockedExports.BrowsingContext;
openWebLinkIn: (
url: string,
where: 'current' | 'tab' | 'tabshifted' | 'window' | 'save',
options?: Partial<{
// Not all possible options are present, please add more if/when needed.
userContextId: number;
forceNonPrivate: boolean;
relatedToCurrent: boolean;
resolveOnContentBrowserCreated: (contentBrowser: MockedExports.ChromeBrowser) => unknown;
}>
) => void;
openTrustedLinkIn: (
url: string,
where: 'current' | 'tab' | 'tabshifted' | 'window' | 'save',
options?: Partial<{
// Not all possible options are present, please add more if/when needed.
userContextId: number;
forceNonPrivate: boolean;
relatedToCurrent: boolean;
resolveOnContentBrowserCreated: (contentBrowser: MockedExports.ChromeBrowser) => unknown;
}>
) => void;
}
declare class ChromeWorker extends Worker {}
declare interface MenuListElement extends XULElement {
value: string;
disabled: boolean;
}
declare interface XULCommandEvent extends Event {
target: XULElement;
}
declare interface XULElementWithCommandHandler {
addEventListener: (type: 'command', handler: (event: XULCommandEvent) => void, isCapture?: boolean) => void;
removeEventListener: (type: 'command', handler: (event: XULCommandEvent) => void, isCapture?: boolean) => void;
}
declare type nsIPrefBranch = MockedExports.nsIPrefBranch;
// chrome context-only DOM isInstance method
// XXX: This hackishly extends Function because there is no way to extend DOM constructors.
// Callers should manually narrow the type when needed.
// See also https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/222
interface Function {
isInstance(obj: any): boolean;
}

View file

@ -800,7 +800,7 @@
if (tabsTarget === gBrowser.tabs.at(-1)) {
newIndex++;
}
gBrowser.moveTabTo(draggedTab, newIndex, { forceUngrouped: true });
gBrowser.moveTabTo(draggedTab, { tabIndex: newIndex, forceUngrouped: true });
}
}
}

View file

@ -255,7 +255,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
if (!this.containerSpecificEssentials) {
container = 0;
}
let essentialsContainer = document.querySelector(`.zen-essentials-container[container="${container}"]`);
let essentialsContainer = document.querySelector(`.zen-essentials-container[container="${container}"]:not([clone])`);
if (!essentialsContainer) {
essentialsContainer = document.createXULElement('vbox');
essentialsContainer.className = 'zen-essentials-container zen-workspace-tabs-section';
@ -657,7 +657,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
if (this._initialTab) {
this.moveTabToWorkspace(this._initialTab, this.activeWorkspace);
gBrowser.selectedTab = this._initialTab;
gBrowser.moveTabTo(this._initialTab, 0, { forceUngrouped: true });
gBrowser.moveTabTo(this._initialTab, { forceUngrouped: true, tabIndex: 0 });
this._initialTab._possiblyEmpty = false;
this._initialTab = null;
}
@ -1735,23 +1735,26 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
const workspaces = await this._workspaces();
const newWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === newWorkspace.uuid);
const clonedEssentials = [];
const essentialsContainerMap = {};
if (shouldAnimate) {
if (shouldAnimate && this.containerSpecificEssentials) {
for (const workspace of workspaces.workspaces) {
const essentialsContainer = this.getEssentialsSection(workspace.containerTabId);
if (clonedEssentials[clonedEssentials.length - 1]?.contextId == workspace.containerTabId) {
clonedEssentials[clonedEssentials.length - 1].repeat++;
clonedEssentials[clonedEssentials.length - 1].workspaces.push(workspace);
continue;
}
essentialsContainer.setAttribute('hidden', 'true');
const essentialsClone = essentialsContainer.cloneNode(true);
essentialsClone.removeAttribute('hidden');
essentialsClone.setAttribute('cloned', 'true');
clonedEssentials.push({
container: essentialsClone,
workspaceId: workspace.uuid,
workspaces: [workspace],
contextId: workspace.containerTabId,
originalContainer: essentialsContainer,
repeat: 0,
});
essentialsContainer.parentNode.appendChild(essentialsClone);
// +0 to convert null to 0
essentialsContainerMap[workspace.containerTabId + 0] = essentialsContainer;
}
}
for (const element of document.querySelectorAll('.zen-workspace-tabs-section')) {
@ -1778,16 +1781,57 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
)
);
if (element.parentNode.id === 'zen-current-workspace-indicator-container') {
// Get essential container clone for this workspace
const clonedEssential = clonedEssentials.find((cloned) => cloned.workspaceId === elementWorkspaceId);
if (clonedEssential && !clonedEssential.animating) {
clonedEssential.animating = true; // Avoid motion hanging due to animating the same element twice
}
if (offset === 0) {
element.setAttribute('active', 'true');
if (tabToSelect != gBrowser.selectedTab) {
gBrowser.selectedTab = tabToSelect;
}
} else {
element.removeAttribute('active');
}
}
if (this.containerSpecificEssentials) {
// Animate essentials
for (const cloned of clonedEssentials) {
const container = cloned.container;
const essentialsWorkspacess = cloned.workspaces;
const repeats = cloned.repeat;
const containerId = cloned.contextId;
// Animate like the workspaces above expect essentials are a bit more
// complicated because they are not based on workspaces but on containers
// So, if we have the following arangement:
// | [workspace1] [workspace2] [workspace3] [workspace4]
// | [container1] [container1] [container2] [container1]
// And if we are changing from workspace 1 to workspace 4,
// we should be doing the following:
// First container (repeat 2 times) will stay in place until
// we reach container 3, then animate to the left and container 2
// also move to the left after that while container 1 in workspace 4
// will slide in from the right
// Get the index from first and last workspace
const firstWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === essentialsWorkspacess[0].uuid);
const lastWorkspaceIndex = workspaces.workspaces.findIndex(
(w) => w.uuid === essentialsWorkspacess[essentialsWorkspacess.length - 1].uuid
);
const isGoingLeft = newWorkspaceIndex > lastWorkspaceIndex;
const isGoingInsideSameContainer = essentialsWorkspacess.some((w) => w.uuid === newWorkspace.uuid);
if (isGoingInsideSameContainer) {
continue; // We dont want to animate if we are going inside the same container
}
const firstOffset = -(newWorkspaceIndex - firstWorkspaceIndex - (isGoingLeft ? repeats : -repeats)) * 100;
const lastOffset = -(newWorkspaceIndex - lastWorkspaceIndex - (isGoingLeft ? repeats : -repeats)) * 100;
const newTransform = `translateX(${firstOffset}%)`;
const existingTransform = `translateX(${lastOffset}%)`;
const stepsInBetween = Math.abs(lastWorkspaceIndex - firstWorkspaceIndex);
if (shouldAnimate) {
container.style.transform = newTransform;
animations.push(
gZenUIManager.motion.animate(
clonedEssential.container,
container,
{
transform: existingTransform ? [existingTransform, newTransform] : newTransform,
transform: [existingTransform, new Array(stepsInBetween).fill(newTransform).join(',')],
},
{
type: 'spring',
@ -1799,15 +1843,6 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
}
}
if (offset === 0) {
element.setAttribute('active', 'true');
if (tabToSelect != gBrowser.selectedTab) {
gBrowser.selectedTab = tabToSelect;
}
} else {
element.removeAttribute('active');
}
}
await Promise.all(animations);
if (shouldAnimate) {
for (const cloned of clonedEssentials) {
@ -2391,6 +2426,9 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
const containers = [...essentialsContainer, ...pinnedContainers, ...normalContainers];
for (const container of containers) {
if (container.hasAttribute('cloned')) {
continue;
}
for (const tab of container.children) {
if (tab.tagName === 'tab') {
tabs.push(tab);