aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-04-24 17:01:25 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:55:02 +0200
commit0b632445a933644c7eb10015eb04b7c21d562900 (patch)
tree5bd1fe200cc747086220dbcba90097bfe2cc4cdc /packages/main/src
parentchore(deps): upgrade dependencies (diff)
downloadsophie-0b632445a933644c7eb10015eb04b7c21d562900.tar.gz
sophie-0b632445a933644c7eb10015eb04b7c21d562900.tar.zst
sophie-0b632445a933644c7eb10015eb04b7c21d562900.zip
refactor: reduce service switcher tearing
We render the location bar and notification banners separately for each service and keep track of the BrowserView size separately for each service to reduce the tearing that appears when people switch services. The tearing cannot be eliminated completely, because it comes from the separation between the main and renderer processes. But we can at least try and reduce the IPC round-tripping and layout calculations required to accurately position the services. This approach has an overhead compared to the single BrowserViewPlaceholder approach, because the renderer process has to layout the location bar and notification for all services, not only the selected one. The number of IPC messages during windows resize is also increased. To compensate, we increase the throttle interval for resize IPC messages and let electron itself resize the BrowserView between IPC updates. (We must still keep pumping IPC messages during window resize, because, e.g., changes in notification banner size due to re-layouting will still affect the required BrowserView size). If further reduction of IPC traffic is needed, we could implement batching for resize IPC messages and more intelligent throttling via a token bucker mechanism. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'packages/main/src')
-rw-r--r--packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts3
-rw-r--r--packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts20
-rw-r--r--packages/main/src/infrastructure/electron/types.ts4
-rw-r--r--packages/main/src/reactions/loadServices.ts8
-rw-r--r--packages/main/src/stores/MainStore.ts19
-rw-r--r--packages/main/src/stores/Service.ts26
6 files changed, 47 insertions, 33 deletions
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts b/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts
index edc6592..b0db115 100644
--- a/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts
+++ b/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts
@@ -153,6 +153,9 @@ export default class ElectronMainWindow implements MainWindow {
153 } 153 }
154 if (serviceView instanceof ElectronServiceView) { 154 if (serviceView instanceof ElectronServiceView) {
155 this.browserWindow.setBrowserView(serviceView.browserView); 155 this.browserWindow.setBrowserView(serviceView.browserView);
156 // If this `BrowserView` hasn't been attached previously,
157 // we must update its bounds _after_ attaching for the resizing to take effect.
158 serviceView.updateBounds();
156 return; 159 return;
157 } 160 }
158 throw new TypeError( 161 throw new TypeError(
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts
index 2e64269..3118efc 100644
--- a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts
+++ b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts
@@ -18,7 +18,6 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { BrowserViewBounds } from '@sophie/shared';
22import { BrowserView } from 'electron'; 21import { BrowserView } from 'electron';
23 22
24import type Service from '../../../stores/Service'; 23import type Service from '../../../stores/Service';
@@ -39,7 +38,7 @@ export default class ElectronServiceView implements ServiceView {
39 readonly browserView: BrowserView; 38 readonly browserView: BrowserView;
40 39
41 constructor( 40 constructor(
42 service: Service, 41 private readonly service: Service,
43 resources: Resources, 42 resources: Resources,
44 partition: ElectronPartition, 43 partition: ElectronPartition,
45 private readonly parent: ElectronViewFactory, 44 private readonly parent: ElectronViewFactory,
@@ -56,6 +55,13 @@ export default class ElectronServiceView implements ServiceView {
56 }); 55 });
57 56
58 this.browserView.setBackgroundColor('#fff'); 57 this.browserView.setBackgroundColor('#fff');
58 this.browserView.setAutoResize({
59 width: true,
60 height: true,
61 });
62 // Util we first attach `browserView` to a `BrowserWindow`,
63 // `setBounds` calls will be ignored, so there's no point in callind `updateBounds` here.
64 // It will be called by `ElectronMainWindow` when we first attach this service to it.
59 65
60 const { webContents } = this.browserView; 66 const { webContents } = this.browserView;
61 67
@@ -191,13 +197,17 @@ export default class ElectronServiceView implements ServiceView {
191 this.browserView.webContents.toggleDevTools(); 197 this.browserView.webContents.toggleDevTools();
192 } 198 }
193 199
194 setBounds(bounds: BrowserViewBounds): void { 200 updateBounds(): void {
195 this.browserView.setBounds(bounds); 201 const { x, y, width, height, hasBounds } = this.service;
202 if (!hasBounds) {
203 return;
204 }
205 this.browserView.setBounds({ x, y, width, height });
196 } 206 }
197 207
198 dispose(): void { 208 dispose(): void {
199 this.parent.unregisterServiceView(this.webContentsId);
200 setImmediate(() => { 209 setImmediate(() => {
210 this.parent.unregisterServiceView(this.webContentsId);
201 // Undocumented electron API, see e.g., https://github.com/electron/electron/issues/29626 211 // Undocumented electron API, see e.g., https://github.com/electron/electron/issues/29626
202 ( 212 (
203 this.browserView.webContents as unknown as { destroy(): void } 213 this.browserView.webContents as unknown as { destroy(): void }
diff --git a/packages/main/src/infrastructure/electron/types.ts b/packages/main/src/infrastructure/electron/types.ts
index 1321048..92ca9ad 100644
--- a/packages/main/src/infrastructure/electron/types.ts
+++ b/packages/main/src/infrastructure/electron/types.ts
@@ -18,8 +18,6 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { BrowserViewBounds } from '@sophie/shared';
22
23import type MainStore from '../../stores/MainStore'; 21import type MainStore from '../../stores/MainStore';
24import type Profile from '../../stores/Profile'; 22import type Profile from '../../stores/Profile';
25import type Service from '../../stores/Service'; 23import type Service from '../../stores/Service';
@@ -67,7 +65,7 @@ export interface ServiceView {
67 65
68 toggleDeveloperTools(): void; 66 toggleDeveloperTools(): void;
69 67
70 setBounds(bounds: BrowserViewBounds): void; 68 updateBounds(): void;
71 69
72 dispose(): void; 70 dispose(): void;
73} 71}
diff --git a/packages/main/src/reactions/loadServices.ts b/packages/main/src/reactions/loadServices.ts
index 4ef6131..f56ac62 100644
--- a/packages/main/src/reactions/loadServices.ts
+++ b/packages/main/src/reactions/loadServices.ts
@@ -18,7 +18,7 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { autorun, reaction } from 'mobx'; 21import { reaction } from 'mobx';
22import { addDisposer } from 'mobx-state-tree'; 22import { addDisposer } from 'mobx-state-tree';
23 23
24import type { 24import type {
@@ -94,7 +94,6 @@ export default function loadServices(
94 throw new Error(`Missing Partition ${profileId}`); 94 throw new Error(`Missing Partition ${profileId}`);
95 } 95 }
96 view = viewFactory.createServiceView(service, partition); 96 view = viewFactory.createServiceView(service, partition);
97 view.setBounds(store.browserViewBounds);
98 servicesToViews.set(serviceId, view); 97 servicesToViews.set(serviceId, view);
99 service.setServiceView(view); 98 service.setServiceView(view);
100 const { urlToLoad } = service; 99 const { urlToLoad } = service;
@@ -133,12 +132,7 @@ export default function loadServices(
133 }, 132 },
134 ); 133 );
135 134
136 const resizeDisposer = autorun(() => {
137 store.visibleService?.serviceView?.setBounds(store.browserViewBounds);
138 });
139
140 addDisposer(store, () => { 135 addDisposer(store, () => {
141 resizeDisposer();
142 disposer(); 136 disposer();
143 store.mainWindow?.setServiceView(undefined); 137 store.mainWindow?.setServiceView(undefined);
144 servicesToViews.forEach((serviceView, serviceId) => { 138 servicesToViews.forEach((serviceView, serviceId) => {
diff --git a/packages/main/src/stores/MainStore.ts b/packages/main/src/stores/MainStore.ts
index d717bed..9affbd0 100644
--- a/packages/main/src/stores/MainStore.ts
+++ b/packages/main/src/stores/MainStore.ts
@@ -18,9 +18,9 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { Action, BrowserViewBounds } from '@sophie/shared'; 21import type { Action } from '@sophie/shared';
22import type { ResourceKey } from 'i18next'; 22import type { ResourceKey } from 'i18next';
23import { applySnapshot, flow, Instance, types } from 'mobx-state-tree'; 23import { flow, Instance, types } from 'mobx-state-tree';
24 24
25import type I18nStore from '../i18n/I18nStore'; 25import type I18nStore from '../i18n/I18nStore';
26import type { UseTranslationResult } from '../i18n/I18nStore'; 26import type { UseTranslationResult } from '../i18n/I18nStore';
@@ -37,15 +37,6 @@ const log = getLogger('MainStore');
37 37
38const MainStore = types 38const MainStore = types
39 .model('MainStore', { 39 .model('MainStore', {
40 browserViewBounds: types.optional(
41 types.model('BrowserViewBounds', {
42 x: 0,
43 y: 0,
44 width: 0,
45 height: 0,
46 }),
47 {},
48 ),
49 shared: types.optional(SharedStore, {}), 40 shared: types.optional(SharedStore, {}),
50 i18n: types.frozen<I18nStore | undefined>(), 41 i18n: types.frozen<I18nStore | undefined>(),
51 }) 42 })
@@ -83,9 +74,6 @@ const MainStore = types
83 }), 74 }),
84 ) 75 )
85 .actions((self) => ({ 76 .actions((self) => ({
86 setBrowserViewBounds(bounds: BrowserViewBounds): void {
87 applySnapshot(self.browserViewBounds, bounds);
88 },
89 setMainWindow(mainWindow: MainWindow | undefined): void { 77 setMainWindow(mainWindow: MainWindow | undefined): void {
90 self.mainWindow = mainWindow; 78 self.mainWindow = mainWindow;
91 }, 79 },
@@ -121,9 +109,6 @@ const MainStore = types
121 .actions((self) => ({ 109 .actions((self) => ({
122 dispatch(action: Action): void { 110 dispatch(action: Action): void {
123 switch (action.action) { 111 switch (action.action) {
124 case 'set-browser-view-bounds':
125 self.setBrowserViewBounds(action.browserViewBounds);
126 break;
127 case 'set-selected-service-id': 112 case 'set-selected-service-id':
128 self.settings.setSelectedServiceId(action.serviceId); 113 self.settings.setSelectedServiceId(action.serviceId);
129 break; 114 break;
diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts
index 1d46dc9..8ba8098 100644
--- a/packages/main/src/stores/Service.ts
+++ b/packages/main/src/stores/Service.ts
@@ -25,6 +25,7 @@ import {
25 defineServiceModel, 25 defineServiceModel,
26 ServiceAction, 26 ServiceAction,
27 type ServiceStateSnapshotIn, 27 type ServiceStateSnapshotIn,
28 type BrowserViewBounds,
28} from '@sophie/shared'; 29} from '@sophie/shared';
29import { type Instance, getSnapshot, cast, flow } from 'mobx-state-tree'; 30import { type Instance, getSnapshot, cast, flow } from 'mobx-state-tree';
30 31
@@ -41,8 +42,18 @@ const Service = defineServiceModel(ServiceSettings)
41 .volatile( 42 .volatile(
42 (): { 43 (): {
43 serviceView: ServiceView | undefined; 44 serviceView: ServiceView | undefined;
45 x: number;
46 y: number;
47 width: number;
48 height: number;
49 hasBounds: boolean;
44 } => ({ 50 } => ({
45 serviceView: undefined, 51 serviceView: undefined,
52 x: 0,
53 y: 0,
54 width: 0,
55 height: 0,
56 hasBounds: false,
46 }), 57 }),
47 ) 58 )
48 .views((self) => ({ 59 .views((self) => ({
@@ -59,10 +70,20 @@ const Service = defineServiceModel(ServiceSettings)
59 })) 70 }))
60 .views((self) => ({ 71 .views((self) => ({
61 get shouldBeVisible(): boolean { 72 get shouldBeVisible(): boolean {
62 return self.shouldBeLoaded && !self.hasError; 73 // Do not attach service views for which we don't know the appropriate frame size,
74 // because they will just appear in a random location until the frame size is determined.
75 return self.shouldBeLoaded && !self.hasError && self.hasBounds;
63 }, 76 },
64 })) 77 }))
65 .actions((self) => ({ 78 .actions((self) => ({
79 setBrowserViewBounds(bounds: BrowserViewBounds): void {
80 self.x = bounds.x;
81 self.y = bounds.y;
82 self.width = bounds.width;
83 self.height = bounds.height;
84 self.hasBounds = true;
85 self.serviceView?.updateBounds();
86 },
66 setServiceView(serviceView: ServiceView | undefined): void { 87 setServiceView(serviceView: ServiceView | undefined): void {
67 self.serviceView = serviceView; 88 self.serviceView = serviceView;
68 }, 89 },
@@ -263,6 +284,9 @@ const Service = defineServiceModel(ServiceSettings)
263 .actions((self) => ({ 284 .actions((self) => ({
264 dispatch(action: ServiceAction): void { 285 dispatch(action: ServiceAction): void {
265 switch (action.action) { 286 switch (action.action) {
287 case 'set-browser-view-bounds':
288 self.setBrowserViewBounds(action.browserViewBounds);
289 break;
266 case 'back': 290 case 'back':
267 self.goBack(); 291 self.goBack();
268 break; 292 break;