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
64 changes: 64 additions & 0 deletions electron/hudWindowBounds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
clampHudBoundsToWorkArea,
clampHudContentToWorkArea,
hudBarAnchor,
hudDragDestination,
hudResizeBounds,
parseHudContentRect,
Expand Down Expand Up @@ -220,6 +221,69 @@ describe("hud resize bounds", () => {
});
});

describe("hud bar home (#1136)", () => {
/** The stack with a popover of `height` open above the bar, centred and bottom-anchored. */
const stackWith = (height: number) => {
const stackHeight = BAR.height + 8 + height;
return {
x: BAR.x,
y: WINDOW.height - BAR_BOTTOM_INSET - stackHeight,
width: BAR.width,
height: stackHeight,
};
};
/** A re-clamp as windows.ts runs it once the bar has a home: same size, placed from home. */
const placeFromHome = (
bounds: typeof WINDOW,
content: typeof BAR,
home: { x: number; y: number },
) =>
hudResizeBounds({
bounds,
previousContent: content,
anchor: home,
width: bounds.width,
height: bounds.height,
nextContent: content,
workArea: WORK_AREA,
});

it("gives the same point for the bar and for a stack with a popover above it", () => {
expect(hudBarAnchor(WINDOW, stackWith(320))).toEqual(hudBarAnchor(WINDOW, BAR));
expect(hudBarAnchor(WINDOW, BAR)).toEqual({
x: WINDOW.x + BAR.x + BAR.width / 2,
y: WINDOW.y + BAR.y + BAR.height,
});
});

it("puts the bar back where it was dropped once a popover that pushed it closes", () => {
// The report: the bar dropped near the top of the screen, its top at y 200.
const dropped = dragTo(0, 200 - BAR.y - WINDOW.y);
expect(barOnScreen(dropped).top).toBe(200);
const home = hudBarAnchor(dropped, BAR);

// The language menu (320 px) has no room above the bar: the window moves down to fit it…
const language = placeFromHome(dropped, stackWith(320), home);
expect(language.y + stackWith(320).y).toBe(WORK_AREA.y);
expect(barOnScreen(language).top).toBeGreaterThan(200);
// …and back once it closes. It used to stay down, at 268 in the report.
const closed = placeFromHome(language, BAR, home);
expect(barOnScreen(closed).top).toBe(200);

// Device settings (470 px) pushes from home, not from where the menu left it, and
// returns there too.
const device = placeFromHome(closed, stackWith(470), home);
expect(device.y + stackWith(470).y).toBe(WORK_AREA.y);
expect(barOnScreen(placeFromHome(device, BAR, home)).top).toBe(200);
});

it("leaves a bar with room above it alone when a popover opens", () => {
const atBottom = dragTo(0, 4000);
const home = hudBarAnchor(atBottom, BAR);
expect(placeFromHome(atBottom, stackWith(470), home)).toEqual(atBottom);
});
});

describe("hud content rect parsing", () => {
it("accepts a measured rect", () => {
expect(parseHudContentRect({ x: 185.5, y: 612, width: 560, height: 64 })).toEqual({
Expand Down
29 changes: 23 additions & 6 deletions electron/hudWindowBounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ export function clampHudBoundsToWorkArea(
: clampRectToWorkArea(bounds, workArea, HUD_WINDOW_MIN);
}

/**
* Screen point of the bar's bottom-centre. The stack is centred on the bar and ends on
* its bottom edge, so any stack rect gives the same point, a popover open above it or
* not. Without a rect, the window's bottom-centre.
*/
export function hudBarAnchor(
bounds: EditorWindowRect,
content: HudContentRect | null,
): { x: number; y: number } {
return content
? { x: bounds.x + content.x + content.width / 2, y: bounds.y + content.y + content.height }
: { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height };
}

/**
* Where a resize re-anchors the window: the bar's bottom-centre stays under
* where it was, so switching between the horizontal bar and the tall vertical
Expand All @@ -98,11 +112,18 @@ export function clampHudBoundsToWorkArea(
* (LaunchWindow.module.css), so the post-resize rect is computed, not guessed.
* The result keeps the new bar fully inside the work area, so a layout flip
* cannot strand the HUD off screen.
*
* `anchor`, when given, is where the bar belongs rather than where it is: the
* point the user left it at. A popover opening near the top of the screen pushes
* the window down so the popover fits, and placing from the anchor instead of the
* pushed window brings the bar back once it closes (#1136).
*/
export function hudResizeBounds(input: {
bounds: EditorWindowRect;
/** Bar rect in the window as it currently is; null before the first report. */
previousContent: HudContentRect | null;
/** The bar's home bottom-centre; null to keep it where it is now. */
anchor?: { x: number; y: number } | null;
width: number;
height: number;
/** Bar rect in the requested size; null for a malformed message. */
Expand All @@ -114,12 +135,8 @@ export function hudResizeBounds(input: {
const width = Math.max(1, Math.round(input.width));
const height = Math.max(1, Math.round(input.height));

const anchorCenterX = input.previousContent
? input.bounds.x + input.previousContent.x + input.previousContent.width / 2
: input.bounds.x + input.bounds.width / 2;
const anchorBottomY = input.previousContent
? input.bounds.y + input.previousContent.y + input.previousContent.height
: input.bounds.y + input.bounds.height;
const { x: anchorCenterX, y: anchorBottomY } =
input.anchor ?? hudBarAnchor(input.bounds, input.previousContent);

const x = input.nextContent
? anchorCenterX - (input.nextContent.x + input.nextContent.width / 2)
Expand Down
25 changes: 24 additions & 1 deletion electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
clampHudBoundsToWorkArea,
HUD_WINDOW_MIN,
type HudContentRect,
hudBarAnchor,
hudContentScreenRect,
hudDragDestination,
hudResizeBounds,
Expand Down Expand Up @@ -174,6 +175,13 @@ let hudDragOrigin: { x: number; y: number } | null = null;
// renderer's first measurement.
let hudContentRect: HudContentRect | null = null;

// Where the user left the bar: its bottom-centre on screen, set by a drag and by the first
// resize. A re-clamp places the bar from here, never from where the window was last pushed:
// a popover opening near the top of the screen pushes the window down so the popover fits,
// and starting from the pushed window left the bar there once the popover closed, a little
// further down with each popover (#1136). Null until the first resize.
let hudBarHome: { x: number; y: number } | null = null;

// Linux never makes the HUD click-through (setHudMouseEventsEnabled in LaunchWindow), so
// an overhanging reserve would swallow clicks on the panels and docks under it. There the
// content rect is ignored and the whole window stays inside the work area.
Expand All @@ -193,7 +201,18 @@ function reclampHud() {
if (!win || win.isDestroyed() || win.isMinimized() || hudDragOrigin) return;

const bounds = hudBounds(win);
const next = clampHudBoundsToWorkArea(bounds, hudContentRect, hudWorkAreaFor(bounds));
const workArea = hudWorkAreaFor(bounds);
const next = hudBarHome
? hudResizeBounds({
bounds,
previousContent: hudContentRect,
anchor: hudBarHome,
width: bounds.width,
height: bounds.height,
nextContent: hudContentRect,
workArea,
})
: clampHudBoundsToWorkArea(bounds, hudContentRect, workArea);
if (!sameRect(next, bounds)) {
win.setBounds(next, false);
}
Expand Down Expand Up @@ -334,6 +353,7 @@ ipcMain.on("hud-overlay-drag-to", (_event, deltaX: number, deltaY: number) => {
// setBounds with the HUD's own size, not setPosition, which re-applies the size read
// back from the window and grows it every frame under fractional scaling.
hudOverlayWindow.setBounds(next, false);
hudBarHome = hudBarAnchor(next, hudContentRect);
});

ipcMain.on("hud-overlay-drag-end", () => {
Expand Down Expand Up @@ -376,9 +396,11 @@ ipcMain.on("hud-overlay-set-size", (_event, width: number, height: number, conte

const bounds = hudBounds(hudOverlayWindow);
const nextContent = HUD_CLAMPS_CONTENT ? parseHudContentRect(content) : null;
hudBarHome ??= hudBarAnchor(bounds, hudContentRect);
const next = hudResizeBounds({
bounds,
previousContent: hudContentRect,
anchor: hudBarHome,
width,
height,
nextContent,
Expand Down Expand Up @@ -496,6 +518,7 @@ export function createHudOverlayWindow(): BrowserWindow {
hudDragOrigin = null;
// The next HUD is a new window: a rect measured in this one would misplace it.
hudContentRect = null;
hudBarHome = null;
stopHudCursorPoll();
}
});
Expand Down
Loading