Skip to content

Commit 7c2bff4

Browse files
committed
Merge remote-tracking branch 'origin/master' into HEAD
# Conflicts: # src/main/sessionwindow/sessionwindow.ts # test/unit/sessionwindow-dispose.test.ts
2 parents e20e732 + 9023161 commit 7c2bff4

10 files changed

Lines changed: 195 additions & 13 deletions

File tree

.github/workflows/publish.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ jobs:
3434
matrix:
3535
cfg:
3636
- { platform: linux-64, platform_name: Linux x64, os: ubuntu-latest, build_platform: linux-64, conda_platform: linux-64 }
37-
- { platform: osx-64, platform_name: macOS x64, os: macos-latest, build_platform: osx-64, conda_platform: osx-64 }
37+
# Native Intel runner: macos-latest is arm64, and packing the osx-64
38+
# env there runs its x86_64 Python under Rosetta, which aborts conda
39+
# pack intermittently (#1018). macos-26-intel is the newest x86_64
40+
# image (GitHub offers no macos-latest-intel; -latest is arm64 only).
41+
- { platform: osx-64, platform_name: macOS x64, os: macos-26-intel, build_platform: osx-64, conda_platform: osx-64 }
3842
- { platform: osx-arm64, platform_name: macOS arm64, os: macos-latest, build_platform: osx-64, conda_platform: osx-arm64 }
3943
- { platform: win-64, platform_name: Windows x64, os: windows-latest, build_platform: win-64, conda_platform: win-64 }
4044

src/main/eventtypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export enum EventTypeRenderer {
9696
SetRunningServerList = 'set-running-server-list',
9797
SetTitle = 'set-title',
9898
SetActive = 'set-active',
99+
SetMaximized = 'set-maximized',
99100
ShowServerStatus = 'show-server-status',
100101
ShowServerNotificationBadge = 'show-server-notification-badge',
101102
SetRecentSessionList = 'set-recent-session-list',

src/main/sessionwindow/sessionwindow.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export class SessionWindow implements IDisposable {
182182
});
183183
this._window.on('blur', () => {
184184
titleBarView.deactivate();
185+
this._hideEnvSelectPopup();
185186
});
186187

187188
titleBarView.load();
@@ -192,12 +193,16 @@ export class SessionWindow implements IDisposable {
192193
// this._labView freshly means an env switch that recreates the labView cannot
193194
// leave a stale, disposed webContents behind, nor accumulate a handler per switch.
194195
this._window.webContents.on('focus', () => {
195-
const wc = this._labView?.view?.webContents;
196-
if (wc && !wc.isDestroyed()) wc.focus();
196+
const wc = this.contentView?.webContents;
197+
if (wc && !wc.isDestroyed()) {
198+
wc.focus();
199+
}
197200
});
198201
this._titleBarView.view.webContents.on('focus', () => {
199-
const wc = this._labView?.view?.webContents;
200-
if (wc && !wc.isDestroyed()) wc.focus();
202+
const wc = this.contentView?.webContents;
203+
if (wc && !wc.isDestroyed()) {
204+
wc.focus();
205+
}
201206
});
202207

203208
if (this._contentViewType === ContentViewType.Lab) {
@@ -242,12 +247,15 @@ export class SessionWindow implements IDisposable {
242247
this._resizeViewsDelayed();
243248
});
244249
this._window.on('maximize', () => {
250+
this._titleBarView.setMaximized(this._window.isMaximized());
245251
this._resizeViewsDelayed();
246252
});
247253
this._window.on('unmaximize', () => {
254+
this._titleBarView.setMaximized(this._window.isMaximized());
248255
this._resizeViewsDelayed();
249256
});
250257
this._window.on('restore', () => {
258+
this._titleBarView.setMaximized(this._window.isMaximized());
251259
this._resizeViewsDelayed();
252260
});
253261
this._window.on('move', () => {
@@ -394,7 +402,9 @@ export class SessionWindow implements IDisposable {
394402
this._window.contentView.addChildView(labView.view);
395403

396404
labView.view.webContents.on('did-finish-load', () => {
397-
labView.view.webContents.focus();
405+
if (this._window.isFocused()) {
406+
labView.view.webContents.focus();
407+
}
398408
});
399409

400410
labView.load((errorCode: number, errorDescription: string) => {
@@ -1376,18 +1386,23 @@ export class SessionWindow implements IDisposable {
13761386
}
13771387

13781388
const titleBarRect = this._titleBarView.view.getBounds();
1379-
const popupWidth = 600;
13801389
const paddingRight = process.platform === 'darwin' ? 33 : 127;
1390+
// Anchor the popup's right edge near the env button and cap its width to
1391+
// what fits, so on a narrow window it shrinks (the popup content is
1392+
// responsive) instead of hanging off an edge with the search box or the
1393+
// env list clipped.
1394+
const rightEdge = titleBarRect.width - paddingRight;
1395+
const popupWidth = Math.min(600, rightEdge);
13811396
// shorten browser view height if larger than max allowed
13821397
const maxHeight = Math.min(
13831398
this._envSelectPopup.getScrollHeight(),
13841399
defaultEnvSelectPopupHeight
13851400
);
13861401

13871402
this._envSelectPopup.view.view.setBounds({
1388-
x: Math.round(titleBarRect.width - paddingRight - popupWidth),
1403+
x: Math.round(rightEdge - popupWidth),
13891404
y: Math.round(titleBarRect.height),
1390-
width: popupWidth,
1405+
width: Math.round(popupWidth),
13911406
height: Math.round(maxHeight)
13921407
});
13931408
}

src/main/titlebarview/preload.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ const { contextBridge, ipcRenderer } = electron;
55

66
type SetTitleListener = (title: string) => void;
77
type SetActiveListener = (active: boolean) => void;
8+
type SetMaximizedListener = (isMaximized: boolean) => void;
89
type ShowServerStatusListener = (show: boolean) => void;
910
type ShowServerNotificationBadgeListener = (show: boolean) => void;
1011

1112
let onSetTitleListener: SetTitleListener;
1213
let onSetActiveListener: SetActiveListener;
14+
let onSetMaximizedListener: SetMaximizedListener;
1315
let onShowServerStatusListener: ShowServerStatusListener;
1416
let onShowServerNotificationBadgeListener: ShowServerNotificationBadgeListener;
1517

@@ -52,6 +54,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
5254
onSetActive: (callback: SetActiveListener) => {
5355
onSetActiveListener = callback;
5456
},
57+
onSetMaximized: (callback: SetMaximizedListener) => {
58+
onSetMaximizedListener = callback;
59+
},
5560
onShowServerStatus: (callback: ShowServerStatusListener) => {
5661
onShowServerStatusListener = callback;
5762
},
@@ -74,6 +79,12 @@ ipcRenderer.on(EventTypeRenderer.SetActive, (event, active) => {
7479
}
7580
});
7681

82+
ipcRenderer.on(EventTypeRenderer.SetMaximized, (event, isMaximized) => {
83+
if (onSetMaximizedListener) {
84+
onSetMaximizedListener(isMaximized);
85+
}
86+
});
87+
7788
ipcRenderer.on(EventTypeRenderer.ShowServerStatus, (event, show) => {
7889
if (onShowServerStatusListener) {
7990
onShowServerStatusListener(show);

src/main/titlebarview/titlebar.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@
230230
}
231231
});
232232

233+
window.electronAPI.onSetMaximized((isMaximized) => {
234+
maximizeButton.style.display = isMaximized ? 'none' : 'block';
235+
restoreButton.style.display = isMaximized ? 'block' : 'none';
236+
});
237+
233238
window.electronAPI.onShowServerStatus((show) => {
234239
serverButton.style.display = show ? 'flex' : 'none';
235240

@@ -255,15 +260,11 @@
255260
maximizeButton.onclick = (ev) => {
256261
ev.stopPropagation();
257262
window.electronAPI.maximizeWindow();
258-
maximizeButton.style.display = 'none';
259-
restoreButton.style.display = 'block';
260263
};
261264

262265
restoreButton.onclick = (ev) => {
263266
ev.stopPropagation();
264267
window.electronAPI.restoreWindow();
265-
restoreButton.style.display = 'none';
266-
maximizeButton.style.display = 'block';
267268
};
268269

269270
closeButton.onclick = (ev) => {

src/main/titlebarview/titlebarview.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ export class TitleBarView {
6262
);
6363
}
6464

65+
setMaximized(isMaximized: boolean) {
66+
this._view.webContents.send(EventTypeRenderer.SetMaximized, isMaximized);
67+
}
68+
6569
showServerStatus(show: boolean) {
6670
this._view.webContents.send(EventTypeRenderer.ShowServerStatus, show);
6771
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { expect, test } from '@playwright/test';
2+
import { cleanup, launchApp } from './helpers';
3+
4+
// The title bar is drawn inside its own WebContentsView. Window dragging relies
5+
// on `-webkit-app-region: drag` on the `.app-title` element, and every control
6+
// button opts back out with `-webkit-app-region: no-drag`. Dropping either rule
7+
// in a refactor silently breaks window moving or makes the buttons undraggable.
8+
//
9+
// This is only a guard on the computed CSS declaration. It does NOT exercise a
10+
// real window move: synthetic Playwright/CDP mouse events do not drive the OS
11+
// drag on the app-region hit-test, so an actual drag cannot be asserted here.
12+
// The real window move, and the layered-WebContentsView no-drag hit-testing
13+
// (electron/electron#43320), still have to be verified by hand per OS.
14+
test('the title bar keeps its drag region and no-drag controls', async () => {
15+
const { app, userDataDir, jupyterDir } = await launchApp();
16+
try {
17+
// Arrange: find the titlebar page. It has no window title, so it is located
18+
// by the presence of its `#app-title` element, the same element-probe
19+
// pattern the dialog-titlebar test uses.
20+
const deadline = Date.now() + 20_000;
21+
let titlebar;
22+
while (!titlebar && Date.now() < deadline) {
23+
for (const page of app.windows()) {
24+
const hasTitle = await page
25+
.evaluate(() => !!document.getElementById('app-title'))
26+
.catch(() => false);
27+
if (hasTitle) {
28+
titlebar = page;
29+
break;
30+
}
31+
}
32+
if (!titlebar) {
33+
await new Promise(resolve => setTimeout(resolve, 200));
34+
}
35+
}
36+
if (!titlebar) {
37+
throw new Error('title bar page with #app-title never appeared');
38+
}
39+
await titlebar.waitForLoadState('load');
40+
41+
// Act: read the computed app-region of the drag surface and of two control
42+
// buttons. The JS-side property is `webkitAppRegion`; its value is the
43+
// keyword ('drag' / 'no-drag'), not a length.
44+
const regions = await titlebar.evaluate(() => {
45+
const regionOf = (id: string) => {
46+
const el = document.getElementById(id);
47+
if (!el) {
48+
return null;
49+
}
50+
return (getComputedStyle(el) as CSSStyleDeclaration & {
51+
webkitAppRegion?: string;
52+
}).webkitAppRegion;
53+
};
54+
return {
55+
appTitle: regionOf('app-title'),
56+
closeButton: regionOf('close-button'),
57+
serverButton: regionOf('server-button')
58+
};
59+
});
60+
61+
// Assert: the title is draggable and the controls explicitly opt out. If a
62+
// refactor drops the `-webkit-app-region` rules these flip to the inherited
63+
// default ('none') and the test fails.
64+
expect(regions.appTitle).toBe('drag');
65+
expect(regions.closeButton).toBe('no-drag');
66+
expect(regions.serverButton).toBe('no-drag');
67+
} finally {
68+
await app.close();
69+
cleanup(userDataDir, jupyterDir);
70+
}
71+
});

test/unit/preload/titlebarview.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe('titlebarview preload', () => {
2929
'sendMouseEvent',
3030
'onSetTitle',
3131
'onSetActive',
32+
'onSetMaximized',
3233
'onShowServerStatus',
3334
'onShowServerNotificationBadge'
3435
].sort()
@@ -89,6 +90,14 @@ describe('titlebarview preload', () => {
8990
expect(cb).toHaveBeenCalledWith(true);
9091
});
9192

93+
it('onSetMaximized relays SetMaximized to the callback', async () => {
94+
const api = await load();
95+
const cb = vi.fn();
96+
api.onSetMaximized(cb);
97+
rendererHandler(EventTypeRenderer.SetMaximized)({}, true);
98+
expect(cb).toHaveBeenCalledWith(true);
99+
});
100+
92101
it('onShowServerStatus relays ShowServerStatus to the callback', async () => {
93102
const api = await load();
94103
const cb = vi.fn();

test/unit/sessionwindow-dispose.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,32 @@ describe('LabView.labUIReady', () => {
126126
expect(settled).toBe(false);
127127
});
128128
});
129+
130+
describe('SessionWindow._resizeEnvSelectPopup', () => {
131+
it('keeps the popup on screen when the window is narrower than the popup', () => {
132+
// Arrange: a 400px title bar (the window minWidth) is narrower than the
133+
// 600px popup, so an unclamped x would place the popup off the left edge.
134+
const setBounds = vi.fn();
135+
const win = makeWindow({
136+
_envSelectPopupVisible: true,
137+
_titleBarView: {
138+
view: { getBounds: () => ({ width: 400, height: 60 }) }
139+
},
140+
_envSelectPopup: {
141+
getScrollHeight: () => 300,
142+
view: { view: { setBounds } }
143+
}
144+
});
145+
146+
// Act
147+
win._resizeEnvSelectPopup();
148+
149+
// Assert: the whole popup fits inside the window, so neither the search box
150+
// on the left nor the env list on the right is clipped. Clamping x alone
151+
// would only move the clipping to the other edge, so the width is capped too.
152+
expect(setBounds).toHaveBeenCalledTimes(1);
153+
const bounds = setBounds.mock.calls[0][0];
154+
expect(bounds.x).toBeGreaterThanOrEqual(0);
155+
expect(bounds.x + bounds.width).toBeLessThanOrEqual(400);
156+
});
157+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { TitleBarView } from '../../src/main/titlebarview/titlebarview';
3+
import { EventTypeRenderer } from '../../src/main/eventtypes';
4+
5+
// Drive setMaximized without the heavy WebContentsView constructor: create an
6+
// instance linked to the prototype and stub only the webContents.send it uses.
7+
function makeTitleBarView(send: ReturnType<typeof vi.fn>): TitleBarView {
8+
const view = Object.create(TitleBarView.prototype);
9+
view._view = { webContents: { send } };
10+
return view;
11+
}
12+
13+
describe('TitleBarView.setMaximized', () => {
14+
it('sends SetMaximized with true when the window is maximized', () => {
15+
// Arrange
16+
const send = vi.fn();
17+
const titleBarView = makeTitleBarView(send);
18+
19+
// Act
20+
titleBarView.setMaximized(true);
21+
22+
// Assert
23+
expect(send).toHaveBeenCalledWith(EventTypeRenderer.SetMaximized, true);
24+
});
25+
26+
it('sends SetMaximized with false when the window is restored', () => {
27+
// Arrange
28+
const send = vi.fn();
29+
const titleBarView = makeTitleBarView(send);
30+
31+
// Act
32+
titleBarView.setMaximized(false);
33+
34+
// Assert
35+
expect(send).toHaveBeenCalledWith(EventTypeRenderer.SetMaximized, false);
36+
});
37+
});

0 commit comments

Comments
 (0)