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
69 changes: 69 additions & 0 deletions src/components/launch/LaunchWindow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,75 @@ describe("LaunchWindow system language prompt", () => {
});
});

describe("LaunchWindow HUD drag", () => {
beforeEach(() => {
platformState.value = "darwin";
resetLaunchMocks();
resizeCallbacks.length = 0;
vi.stubGlobal("ResizeObserver", CapturingResizeObserver);
// jsdom doesn't implement the Pointer Capture API; stub it so the drag handlers
// (which call set/has/releasePointerCapture) don't throw.
HTMLElement.prototype.setPointerCapture = vi.fn();
HTMLElement.prototype.hasPointerCapture = vi.fn(() => true);
HTMLElement.prototype.releasePointerCapture = vi.fn();
});

afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});

it("suppresses ResizeObserver-driven measurement while dragging, and measures once on release", async () => {
renderLaunchWindow();

const dragHandle = await screen.findByTestId("hud-drag-handle");

// Give the bar a non-zero, changing size so a resize observation would actually
// trigger a `setHudOverlaySize` call if it weren't suppressed during the drag.
const bar = dragHandle.closest("[data-tray-layout]") as HTMLElement | null;
if (bar) {
vi.spyOn(bar, "getBoundingClientRect").mockReturnValue({
top: 700,
left: 200,
right: 600,
bottom: 756,
width: 400,
height: 56,
x: 200,
y: 700,
toJSON: () => ({}),
});
Object.defineProperty(bar, "scrollHeight", { value: 56, configurable: true });
Object.defineProperty(bar, "scrollWidth", { value: 400, configurable: true });
}

const sizeMock = window.electronAPI.setHudOverlaySize as unknown as {
mockClear: () => void;
};
sizeMock.mockClear();

fireEvent.pointerDown(dragHandle, { screenX: 100, screenY: 100 });

// Simulate a ResizeObserver firing mid-drag (e.g. transient reflow) -- this must
// NOT reposition/resize the HUD while the user's pointer is still down.
await act(async () => {
for (const callback of resizeCallbacks) {
callback([], {} as ResizeObserver);
}
});
expect(window.electronAPI.setHudOverlaySize).not.toHaveBeenCalled();

fireEvent.pointerMove(dragHandle, { screenX: 140, screenY: 130 });
fireEvent.pointerUp(dragHandle, { screenX: 140, screenY: 130 });

// Content is re-measured once the drag ends, so a real size change made mid-drag
// still gets picked up promptly.
await waitFor(() => {
expect(window.electronAPI.setHudOverlaySize).toHaveBeenCalled();
});
});
});

describe("LaunchWindow software encoder fallback notice", () => {
beforeEach(() => {
platformState.value = "darwin";
Expand Down
11 changes: 11 additions & 0 deletions src/components/launch/LaunchWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,16 @@ export function LaunchWindow() {
// and scrolls. Measure from the window's bottom-centre (the anchor the main process
// preserves) so fixed bottom/centre offsets keep this stable and it doesn't oscillate.
const lastHudSizeRef = useRef({ width: 0, height: 0 });
const isDraggingHudRef = useRef(false);
const measureHudSize = useCallback(() => {
const barEl = hudBarRef.current;
if (!barEl || !window.electronAPI?.setHudOverlaySize) return;
// While the user is dragging the HUD, ignore content-size measurements. A
// ResizeObserver-driven resize (hud-overlay-set-size) re-centres the window from
// its own bottom-centre anchor, which fights the position "hud-overlay-move-by" is
// actively applying frame-by-frame -- the two IPC channels racing is what produces
// the reported drift. Content size is re-measured once the drag ends instead.
if (isDraggingHudRef.current) return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Breathing room so the drop shadow isn't clipped. TOP_MARGIN must also exceed the
// slack in the bar's `max-h: calc(100vh - 2.5rem)` cap (40px reserved - 20px bottom
Expand Down Expand Up @@ -634,6 +641,7 @@ export function LaunchWindow() {
setHudMouseEventsEnabled(true);
event.currentTarget.setPointerCapture(event.pointerId);
dragLastPositionRef.current = { x: event.screenX, y: event.screenY };
isDraggingHudRef.current = true;
};
const handleHudDragPointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
const lastPosition = dragLastPositionRef.current;
Expand All @@ -650,6 +658,8 @@ export function LaunchWindow() {
event.currentTarget.releasePointerCapture(event.pointerId);
}
setHudMouseEventsEnabled(false);
isDraggingHudRef.current = false;
measureHudSize();
};

return (
Expand Down Expand Up @@ -916,6 +926,7 @@ export function LaunchWindow() {
>
{/* Drag handle */}
<div
data-testid="hud-drag-handle"
className={`flex ${trayLayout === "vertical" ? "h-6 w-8" : "h-8 w-7"} cursor-grab items-center justify-center active:cursor-grabbing ${styles.electronNoDrag}`}
onPointerDown={handleHudDragPointerDown}
onPointerMove={handleHudDragPointerMove}
Expand Down
Loading