Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
78 changes: 78 additions & 0 deletions electron/editorWindowState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
clampRectToWorkArea,
fitEditorRect,
loadEditorWindowState,
resolveEditorCreation,
saveEditorWindowState,
Expand Down Expand Up @@ -42,13 +43,18 @@ describe("editor window state", () => {
it("still maximizes when nothing is saved, and never loads or saves the bench", () => {
const missing = resolveEditorCreation({ isBench: false, saved: null });
expect(missing.maximize).toBe(true);
expect(missing.deferMaximize).toBe(false);
expect(missing.persist).toBe(true);
expect(missing.bounds).toEqual({ width: 1200, height: 800 });

expect(shouldTrackEditorWindow({ windowType: "bench" })).toBe(false);
const bench = resolveEditorCreation({ isBench: true, saved: null });
expect(bench.persist).toBe(false);
expect(bench.maximize).toBe(true);
// Not even under Wayland: a maximize landing mid-run would resize what it measures.
expect(resolveEditorCreation({ isBench: true, saved: null, wayland: true }).deferMaximize).toBe(
false,
);

const dir = tmp();
saveEditorWindowState(dir, { x: 10, y: 20, width: 1280, height: 720, maximized: false });
Expand Down Expand Up @@ -106,3 +112,75 @@ describe("editor window state", () => {
}
});
});

describe("editor window state under Wayland", () => {
// GNOME 46 on a 1920x1080 output: Chromium reports the whole output as the work area,
// although the top bar and the dock take part of it.
const OUTPUT = { x: 0, y: 0, width: 1920, height: 1080 };
// What the v2.1.0-rc.1 pass found on disk after a maximized session (it grew each time).
const DRIFTED = { x: 256, y: -10, width: 1440, height: 1121, maximized: true };

it("keeps a tenth of each dimension free for panels it cannot see", () => {
expect(fitEditorRect(DRIFTED, OUTPUT, true)).toEqual({
x: 256,
y: 54,
width: 1440,
height: 972,
});
// Elsewhere the reported work area is the real one, and is used whole.
expect(fitEditorRect(DRIFTED, OUTPUT, false)).toEqual({
x: 256,
y: 0,
width: 1440,
height: 1080,
});
expect(fitEditorRect(DRIFTED, OUTPUT, false)).toEqual(clampRectToWorkArea(DRIFTED, OUTPUT));
});

it("leaves a rect that already fits as it is", () => {
const rect = { x: 300, y: 120, width: 1200, height: 800 };
expect(fitEditorRect(rect, OUTPUT, true)).toEqual(rect);
});

it("never fits below the editor's minimum size", () => {
const fitted = fitEditorRect(
{ x: 0, y: 0, width: 1024, height: 700 },
{ x: 0, y: 0, width: 800, height: 600 },
true,
);
expect(fitted.width).toBe(800);
expect(fitted.height).toBe(600);
});

it("leaves placement to the compositor and maximizes only once mapped", () => {
const saved = { ...fitEditorRect(DRIFTED, OUTPUT, true), maximized: true };
const creation = resolveEditorCreation({ isBench: false, saved, wayland: true });
expect(creation.bounds).toEqual({ width: 1440, height: 972 });
expect(creation.maximize).toBe(true);
expect(creation.deferMaximize).toBe(true);

const fresh = resolveEditorCreation({ isBench: false, saved: null, wayland: true });
expect(fresh.maximize).toBe(true);
expect(fresh.deferMaximize).toBe(true);
});

it("keeps the saved position and the immediate maximize elsewhere", () => {
const saved = { x: 10, y: 20, width: 1280, height: 720, maximized: true };
const creation = resolveEditorCreation({ isBench: false, saved, wayland: false });
expect(creation.bounds).toEqual({ x: 10, y: 20, width: 1280, height: 720 });
expect(creation.deferMaximize).toBe(false);
});

it("still opens the default window for a hand-edited zero size or non-boolean maximized", () => {
const dir = tmp();
writeFileSync(
path.join(dir, "editor-window.json"),
JSON.stringify({ x: 10, y: 20, width: 0, height: 0, maximized: "yes" }),
);
const saved = loadEditorWindowState(dir);
expect(saved).toBeNull();
const creation = resolveEditorCreation({ isBench: false, saved, wayland: true });
expect(creation.bounds).toEqual({ width: 1200, height: 800 });
expect(creation.maximize).toBe(true);
});
});
70 changes: 62 additions & 8 deletions electron/editorWindowState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,32 +95,86 @@ export function clampRectToWorkArea(
};
}

/**
* Share of each dimension of the reported work area left to panels under Wayland.
*
* Wayland tells a client nothing about panels, so Chromium reports the whole output as the
* work area — GNOME's 32 px top bar and a docked Ubuntu dock included. A normal window as tall
* as the output then runs under them, with its bottom edge off screen, and un-maximizing
* returns to exactly that. A tenth of each dimension (108 px of a 1080 px output) covers a top
* bar and a dock; the compositor still places the window, so only its size is at stake.
*/
export const WAYLAND_PANEL_RESERVE = 0.1;

/** Clamps a normal rect to the work area — under Wayland, to the share panels leave free. */
export function fitEditorRect(
rect: EditorWindowRect,
workArea: DisplayWorkArea,
wayland: boolean,
): EditorWindowRect {
if (!wayland) return clampRectToWorkArea(rect, workArea);
// Centred: the panels may sit on any edge.
const width = Math.floor(workArea.width * (1 - WAYLAND_PANEL_RESERVE));
const height = Math.floor(workArea.height * (1 - WAYLAND_PANEL_RESERVE));
return clampRectToWorkArea(rect, {
x: workArea.x + Math.floor((workArea.width - width) / 2),
y: workArea.y + Math.floor((workArea.height - height) / 2),
width,
height,
});
}

/**
* How long a Wayland editor waits, once the compositor has focused it, before maximizing.
*
* Electron 41's Chromium leaves a window stuck maximized — its restore button and
* `unmaximize()` do nothing — when it asks for maximize while the compositor still has a
* configure without the maximized state on its way: the configure latches Chromium's
* "restore to maximized" flag (electron/electron#53063, fixed in Electron 42+ only). Focus
* means the window is mapped, and the configures that come with mapping (activated, suspended)
* arrive within a frame or two of it; this waits them out with room to spare.
*/
export const WAYLAND_MAXIMIZE_SETTLE_MS = 200;

export function shouldTrackEditorWindow(query: Record<string, string>): boolean {
return query.windowType !== "bench";
}

export function resolveEditorCreation(input: {
isBench: boolean;
saved: EditorWindowState | null;
/** Chromium's Wayland backend: positions are fiction, and maximize must wait for the map. */
wayland?: boolean;
}): {
bounds: { x?: number; y?: number; width: number; height: number };
maximize: boolean;
/** Maximize only once the compositor has mapped the window (see WAYLAND_MAXIMIZE_SETTLE_MS). */
deferMaximize: boolean;
persist: boolean;
} {
if (input.isBench) {
return { bounds: { ...DEFAULT_EDITOR_SIZE }, maximize: true, persist: false };
// The bench never un-maximizes, and a maximize that lands mid-run would resize what it
// measures: it keeps the immediate one everywhere.
return {
bounds: { ...DEFAULT_EDITOR_SIZE },
maximize: true,
deferMaximize: false,
persist: false,
};
}
const deferMaximize = input.wayland === true;
if (!input.saved) {
return { bounds: { ...DEFAULT_EDITOR_SIZE }, maximize: true, persist: true };
return { bounds: { ...DEFAULT_EDITOR_SIZE }, maximize: true, deferMaximize, persist: true };
}
const { width, height } = input.saved;
return {
bounds: {
x: input.saved.x,
y: input.saved.y,
width: input.saved.width,
height: input.saved.height,
},
// A Wayland client cannot place its window and never learns where the compositor put
// it, so a saved position is whatever Chromium assumed: leave placement to the compositor.
bounds: input.wayland
? { width, height }
: { x: input.saved.x, y: input.saved.y, width, height },
maximize: input.saved.maximized,
deferMaximize,
persist: true,
};
}
24 changes: 23 additions & 1 deletion electron/wayland.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { configureWaylandSupport } from "./wayland";
import { configureWaylandSupport, usesWaylandBackend } from "./wayland";

describe("Wayland startup configuration", () => {
it.each([
Expand Down Expand Up @@ -36,3 +36,25 @@ describe("Wayland startup configuration", () => {
expect(commandLine.appendSwitch).not.toHaveBeenCalled();
});
});

describe("Wayland backend detection", () => {
const ozone = (value: string) => ({ getSwitchValue: () => value });
const session = { XDG_SESSION_TYPE: "wayland" };

it.each(["wayland", "auto", ""])("is on in a Linux Wayland session (ozone-platform=%j)", (v) => {
expect(usesWaylandBackend("linux", ozone(v), session)).toBe(true);
expect(usesWaylandBackend("linux", ozone(v), { WAYLAND_DISPLAY: "wayland-0" })).toBe(true);
});

it("is off under Xwayland, when ozone-platform=x11 was passed explicitly", () => {
expect(usesWaylandBackend("linux", ozone("x11"), session)).toBe(false);
});

it("is off in an X11 session", () => {
expect(usesWaylandBackend("linux", ozone(""), { XDG_SESSION_TYPE: "x11" })).toBe(false);
});

it.each(["win32", "darwin"] as const)("is off on %s, whatever the environment", (platform) => {
expect(usesWaylandBackend(platform, ozone("wayland"), session)).toBe(false);
});
});
17 changes: 17 additions & 0 deletions electron/wayland.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ export function isWaylandSession(env: Env): boolean {
return env.XDG_SESSION_TYPE === "wayland" || env.WAYLAND_DISPLAY !== undefined;
}

/**
* Whether Chromium draws this process's windows through its Wayland backend: a Linux Wayland
* session, unless an explicit `--ozone-platform=x11` sent it to Xwayland, which
* configureWaylandSupport leaves alone.
*/
export function usesWaylandBackend(
platform: NodeJS.Platform,
commandLine: Pick<Electron.CommandLine, "getSwitchValue">,
env: Env,
): boolean {
return (
platform === "linux" &&
isWaylandSession(env) &&
commandLine.getSwitchValue("ozone-platform") !== "x11"
);
}

/** Prefer Wayland in a Wayland session, while respecting an explicit backend. */
export function configureWaylandSupport(commandLine: CommandLine, env: Env): void {
if (!isWaylandSession(env)) return;
Expand Down
Loading
Loading