Skip to content

Commit

Permalink
Open initial electron window early and reuse it later
Browse files Browse the repository at this point in the history
This avoids having a rather long pause from Theia start until something
is visibly coming up. In my initial experiment, I observe the empty
window showing up ~1.5s earlier than usual.

This behavior can be controlled via the application config in the
application's package.json. For instance, to turn off, use:

```
  "theia": {
    "target": "electron",
    "frontend": {
      "config": {
        "applicationName": "Theia Electron Example",
        "electron": {
          "showWindowEarly": false
        }
      }
    }
  }
```

Also, we store the background color of the current color theme
to be able to show the correct background color in the initial
window.

Contributed on behalf of STMicroelectronics.

Change-Id: Ib8a6e8221c4f48605c865afc120d62adf6eef902
  • Loading branch information
planger committed Sep 26, 2023
1 parent f038673 commit bd18c66
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
## v1.42.0

- [vsx-registry] added a hint to extension fetching ENOTFOUND errors [#12858](https://github.com/eclipse-theia/theia/pull/12858) - Contributed by STMicroelectronics
- [electron] Open initial electron window early to improve perceived performance [#12897](https://github.com/eclipse-theia/theia/pull/12897) - Contributed on behalf of STMicroelectronics.

## v1.41.0 - 08/31/2023

Expand Down
15 changes: 14 additions & 1 deletion dev-packages/application-package/src/application-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export interface ApplicationConfig {
export type ElectronFrontendApplicationConfig = RequiredRecursive<ElectronFrontendApplicationConfig.Partial>;
export namespace ElectronFrontendApplicationConfig {
export const DEFAULT: ElectronFrontendApplicationConfig = {
windowOptions: {}
windowOptions: {},
showWindowEarly: true
};
export interface Partial {

Expand All @@ -42,6 +43,13 @@ export namespace ElectronFrontendApplicationConfig {
* Defaults to `{}`.
*/
readonly windowOptions?: BrowserWindowConstructorOptions;

/**
* Whether or not to show an empty Electron window as early as possible.
*
* Defaults to `true`.
*/
readonly showWindowEarly?: boolean;
}
}

Expand All @@ -60,6 +68,11 @@ export namespace DefaultTheme {
}
return theme.light;
}
export function defaultBackgroundColor(dark?: boolean): string {
// The default light background color is based on the `colors#editor.background` value from
// `packages/monaco/data/monaco-themes/vscode/dark_vs.json` and the dark background comes from the `light_vs.json`.
return dark ? '#1E1E1E' : '#FFFFFF';
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@
import { PreloadContribution } from './preloader';
import { DEFAULT_BACKGROUND_COLOR_STORAGE_KEY } from '../frontend-application-config-provider';
import { injectable } from 'inversify';
import { DefaultTheme } from '@theia/application-package';

@injectable()
export class ThemePreloadContribution implements PreloadContribution {

initialize(): void {
// The default light background color is based on the `colors#editor.background` value from
// `packages/monaco/data/monaco-themes/vscode/dark_vs.json` and the dark background comes from the `light_vs.json`.
const dark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
const value = window.localStorage.getItem(DEFAULT_BACKGROUND_COLOR_STORAGE_KEY) || (dark ? '#1E1E1E' : '#FFFFFF');
const value = window.localStorage.getItem(DEFAULT_BACKGROUND_COLOR_STORAGE_KEY) || DefaultTheme.defaultBackgroundColor(dark);
document.documentElement.style.setProperty('--theia-editor-background', value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import { WindowTitleService } from '../../browser/window/window-title-service';

import '../../../src/electron-browser/menu/electron-menu-style.css';
import { MenuDto } from '../../electron-common/electron-api';
import { ThemeService } from '../../browser/theming';
import { ThemeChangeEvent } from '../../common/theme';

export namespace ElectronCommands {
export const TOGGLE_DEVELOPER_TOOLS = Command.toDefaultLocalizedCommand({
Expand Down Expand Up @@ -88,6 +90,9 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
@inject(WindowService)
protected readonly windowService: WindowService;

@inject(ThemeService)
protected readonly themeService: ThemeService;

@inject(CustomTitleWidgetFactory)
protected readonly customTitleWidgetFactory: CustomTitleWidgetFactory;

Expand Down Expand Up @@ -123,6 +128,9 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
this.handleToggleMaximized();
});
this.attachMenuBarVisibilityListener();
this.themeService.onDidColorThemeChange(e => {
this.handleThemeChange(e);
});
}

protected attachWindowFocusListener(app: FrontendApplication): void {
Expand Down Expand Up @@ -414,6 +422,11 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
}
}

protected handleThemeChange(e: ThemeChangeEvent): void {
const backgroundColor = window.getComputedStyle(document.body).backgroundColor;
window.electronTheiaCore.setBackgroundColor(backgroundColor);
}

}

@injectable()
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/electron-browser/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
CHANNEL_ON_WINDOW_EVENT, CHANNEL_GET_ZOOM_LEVEL, CHANNEL_SET_ZOOM_LEVEL, CHANNEL_IS_FULL_SCREENABLE, CHANNEL_TOGGLE_FULL_SCREEN,
CHANNEL_IS_FULL_SCREEN, CHANNEL_SET_MENU_BAR_VISIBLE, CHANNEL_REQUEST_CLOSE, CHANNEL_SET_TITLE_STYLE, CHANNEL_RESTART,
CHANNEL_REQUEST_RELOAD, CHANNEL_APP_STATE_CHANGED, CHANNEL_SHOW_ITEM_IN_FOLDER, CHANNEL_READ_CLIPBOARD, CHANNEL_WRITE_CLIPBOARD,
CHANNEL_KEYBOARD_LAYOUT_CHANGED, CHANNEL_IPC_CONNECTION, InternalMenuDto, CHANNEL_REQUEST_SECONDARY_CLOSE
CHANNEL_KEYBOARD_LAYOUT_CHANGED, CHANNEL_IPC_CONNECTION, InternalMenuDto, CHANNEL_REQUEST_SECONDARY_CLOSE, CHANNEL_SET_BACKGROUND_COLOR
} from '../electron-common/electron-api';

// eslint-disable-next-line import/no-extraneous-dependencies
Expand Down Expand Up @@ -101,6 +101,9 @@ const api: TheiaCoreAPI = {
setTitleBarStyle: function (style): void {
ipcRenderer.send(CHANNEL_SET_TITLE_STYLE, style);
},
setBackgroundColor: function (backgroundColor): void {
ipcRenderer.send(CHANNEL_SET_BACKGROUND_COLOR, backgroundColor);
},
minimize: function (): void {
ipcRenderer.send(CHANNEL_MINIMIZE);
},
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/electron-common/electron-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface TheiaCoreAPI {

getTitleBarStyleAtStartup(): Promise<string>;
setTitleBarStyle(style: string): void;
setBackgroundColor(backgroundColor: string): void;
minimize(): void;
isMaximized(): boolean; // TODO: this should really be async, since it blocks the renderer process
maximize(): void;
Expand Down Expand Up @@ -109,6 +110,7 @@ export const CHANNEL_ATTACH_SECURITY_TOKEN = 'AttachSecurityToken';

export const CHANNEL_GET_TITLE_STYLE_AT_STARTUP = 'GetTitleStyleAtStartup';
export const CHANNEL_SET_TITLE_STYLE = 'SetTitleStyle';
export const CHANNEL_SET_BACKGROUND_COLOR = 'SetBackgroundColor';
export const CHANNEL_CLOSE = 'Close';
export const CHANNEL_MINIMIZE = 'Minimize';
export const CHANNEL_MAXIMIZE = 'Maximize';
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/electron-main/electron-api-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ import {
CHANNEL_SET_MENU_BAR_VISIBLE,
CHANNEL_TOGGLE_FULL_SCREEN,
CHANNEL_IS_MAXIMIZED,
CHANNEL_REQUEST_SECONDARY_CLOSE
CHANNEL_REQUEST_SECONDARY_CLOSE,
CHANNEL_SET_BACKGROUND_COLOR
} from '../electron-common/electron-api';
import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application';
import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common';
Expand Down Expand Up @@ -152,6 +153,8 @@ export class TheiaMainApi implements ElectronMainApplicationContribution {

ipcMain.on(CHANNEL_SET_TITLE_STYLE, (event, style) => application.setTitleBarStyle(event.sender, style));

ipcMain.on(CHANNEL_SET_BACKGROUND_COLOR, (event, backgroundColor) => application.setBackgroundColor(event.sender, backgroundColor));

ipcMain.on(CHANNEL_MINIMIZE, event => {
BrowserWindow.fromWebContents(event.sender)?.minimize();
});
Expand Down
44 changes: 40 additions & 4 deletions packages/core/src/electron-main/electron-main-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
// *****************************************************************************

import { inject, injectable, named } from 'inversify';
import { screen, app, BrowserWindow, WebContents, Event as ElectronEvent, BrowserWindowConstructorOptions, nativeImage } from '../../electron-shared/electron';
import { screen, app, BrowserWindow, WebContents, Event as ElectronEvent, BrowserWindowConstructorOptions, nativeImage, nativeTheme } from '../../electron-shared/electron';
import * as path from 'path';
import { Argv } from 'yargs';
import { AddressInfo } from 'net';
import { promises as fs } from 'fs';
import { fork, ForkOptions } from 'child_process';
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
import { DefaultTheme, FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
import URI from '../common/uri';
import { FileUri } from '../node/file-uri';
import { Deferred } from '../common/promise-util';
Expand Down Expand Up @@ -180,10 +180,13 @@ export class ElectronMainApplication {

protected _config: FrontendApplicationConfig | undefined;
protected useNativeWindowFrame: boolean = true;
protected customBackgroundColor?: string;
protected didUseNativeWindowFrameOnStart = new Map<number, boolean>();
protected windows = new Map<number, TheiaElectronWindow>();
protected restarting = false;

protected initialWindow?: BrowserWindow;

get config(): FrontendApplicationConfig {
if (!this._config) {
throw new Error('You have to start the application first.');
Expand All @@ -195,6 +198,7 @@ export class ElectronMainApplication {
this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
this._config = config;
this.hookApplicationEvents();
this.showInitialWindow();
const port = await this.startBackend();
this._backendPort.resolve(port);
await app.whenReady();
Expand Down Expand Up @@ -226,6 +230,15 @@ export class ElectronMainApplication {

public setTitleBarStyle(webContents: WebContents, style: string): void {
this.useNativeWindowFrame = isOSX || style === 'native';
this.saveState(webContents);
}

setBackgroundColor(webContents: WebContents, backgroundColor: string): void {
this.customBackgroundColor = backgroundColor;
this.saveState(webContents);
}

protected saveState(webContents: Electron.WebContents): void {
const browserWindow = BrowserWindow.fromWebContents(webContents);
if (browserWindow) {
this.saveWindowState(browserWindow);
Expand All @@ -242,6 +255,16 @@ export class ElectronMainApplication {
return this.didUseNativeWindowFrameOnStart.get(webContents.id) ? 'native' : 'custom';
}

protected showInitialWindow(): void {
if (this.config.electron.showWindowEarly) {
app.whenReady().then(async () => {
const options = await this.getLastWindowOptions();
this.initialWindow = await this.createWindow({ ...options });
this.initialWindow.show();
});
}
}

protected async launch(params: ElectronMainExecutionParams): Promise<void> {
createYargs(params.argv, params.cwd)
.command('$0 [file]', false,
Expand Down Expand Up @@ -303,6 +326,7 @@ export class ElectronMainApplication {
return {
show: false,
title: this.config.applicationName,
backgroundColor: DefaultTheme.defaultBackgroundColor(this.config.electron.windowOptions?.darkTheme || nativeTheme.shouldUseDarkColors),
minWidth: 200,
minHeight: 120,
webPreferences: {
Expand All @@ -320,18 +344,29 @@ export class ElectronMainApplication {
}

async openDefaultWindow(): Promise<BrowserWindow> {
const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.createWindow()]);
const options = this.getDefaultTheiaWindowOptions();
const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.reuseOrCreateWindow(options)]);
electronWindow.loadURL(uri.withFragment(DEFAULT_WINDOW_HASH).toString(true));
return electronWindow;
}

protected async openWindowWithWorkspace(workspacePath: string): Promise<BrowserWindow> {
const options = await this.getLastWindowOptions();
const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.createWindow(options)]);
const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.reuseOrCreateWindow(options)]);
electronWindow.loadURL(uri.withFragment(encodeURI(workspacePath)).toString(true));
return electronWindow;
}

protected async reuseOrCreateWindow(asyncOptions: MaybePromise<TheiaBrowserWindowOptions>): Promise<BrowserWindow> {
if (!this.initialWindow) {
return this.createWindow(asyncOptions);
}
// reset initial window after having it re-used once
const window = this.initialWindow;
this.initialWindow = undefined;
return window;
}

/** Configures native window creation, i.e. using window.open or links with target "_blank" in the frontend. */
protected configureNativeSecondaryWindowCreation(electronWindow: BrowserWindow): void {
electronWindow.webContents.setWindowOpenHandler(() => {
Expand Down Expand Up @@ -455,6 +490,7 @@ export class ElectronMainApplication {
y: bounds.y,
frame: this.useNativeWindowFrame,
screenLayout: this.getCurrentScreenLayout(),
backgroundColor: this.customBackgroundColor
};
this.electronStore.set('windowstate', options);
} catch (e) {
Expand Down

0 comments on commit bd18c66

Please sign in to comment.