Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: splash screen support for Electron #13505

Merged
merged 4 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

- [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/)

<!-- ## not yet released
## not yet released

<a name="breaking_changes_not_yet_released">[Breaking Changes:](#breaking_changes_not_yet_released)</a> -->
- [core] Splash Screen Support for Electron [#13505](https://github.com/eclipse-theia/theia/pull/13505) - contributed on behalf of Pragmatiqu IT GmbH

<a name="breaking_changes_not_yet_released">[Breaking Changes:](#breaking_changes_not_yet_released)</a>

## v1.48.0 - 03/28/2024

Expand Down
37 changes: 36 additions & 1 deletion dev-packages/application-package/src/application-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,36 @@ export type ElectronFrontendApplicationConfig = RequiredRecursive<ElectronFronte
export namespace ElectronFrontendApplicationConfig {
export const DEFAULT: ElectronFrontendApplicationConfig = {
windowOptions: {},
showWindowEarly: true
showWindowEarly: true,
splashScreenOptions: {}
};
export interface SplashScreenOptions {
/**
* Initial width of the splash screen. Defaults to 640.
*/
width?: number;
/**
* Initial height of the splash screen. Defaults to 480.
*/
height?: number;
/**
* Minimum amount of time in milliseconds to show the splash screen before main window is shown.
* Defaults to 0, i.e. the splash screen will be shown until the frontend application is ready.
*/
minDuration?: number;
/**
* Maximum amount of time in milliseconds before splash screen is removed and main window is shown.
* Defaults to 30000.
*/
maxDuration?: number;
/**
* The content to load in the splash screen.
* Will be resolved from application root.
*
* Mandatory attribute.
*/
content?: string;
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved
}
export interface Partial {

/**
Expand All @@ -50,6 +78,13 @@ export namespace ElectronFrontendApplicationConfig {
* Defaults to `true`.
*/
readonly showWindowEarly?: boolean;

/**
* Configuration options for splash screen.
*
* Defaults to `{}` which results in no splash screen being displayed.
*/
readonly splashScreenOptions?: SplashScreenOptions;
}
}

Expand Down
8 changes: 7 additions & 1 deletion examples/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
"frontend": {
"config": {
"applicationName": "Theia Electron Example",
"reloadOnReconnect": true
"reloadOnReconnect": true,
"electron": {
"splashScreenOptions": {
"content": "resources/theia-logo.svg",
"height": 90
}
}
}
},
"backend": {
Expand Down
32 changes: 32 additions & 0 deletions examples/electron/resources/theia-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 101 additions & 7 deletions packages/core/src/electron-main/electron-main-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ import { AddressInfo } from 'net';
import { promises as fs } from 'fs';
import { existsSync, mkdirSync } from 'fs-extra';
import { fork, ForkOptions } from 'child_process';
import { DefaultTheme, FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
import { DefaultTheme, ElectronFrontendApplicationConfig, FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
import URI from '../common/uri';
import { FileUri } from '../common/file-uri';
import { Deferred } from '../common/promise-util';
import { Deferred, timeout } from '../common/promise-util';
import { MaybePromise } from '../common/types';
import { ContributionProvider } from '../common/contribution-provider';
import { ElectronSecurityTokenService } from './electron-security-token-service';
import { ElectronSecurityToken } from '../electron-common/electron-token';
import Storage = require('electron-store');
import { Disposable, DisposableCollection, isOSX, isWindows } from '../common';
import { CancellationTokenSource, Disposable, DisposableCollection, isOSX, isWindows } from '../common';
import { DEFAULT_WINDOW_HASH, WindowSearchParams } from '../common/window';
import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory } from './theia-electron-window';
import { ElectronMainApplicationGlobals } from './electron-main-constants';
Expand Down Expand Up @@ -182,6 +182,7 @@ export class ElectronMainApplication {
protected windows = new Map<number, TheiaElectronWindow>();
protected restarting = false;

/** Used to temporarily store the reference to an early created main window */
protected initialWindow?: BrowserWindow;

get config(): FrontendApplicationConfig {
Expand Down Expand Up @@ -287,18 +288,110 @@ export class ElectronMainApplication {
return this.didUseNativeWindowFrameOnStart.get(webContents.id) ? 'native' : 'custom';
}

protected async determineSplashScreenBounds(initialWindowBounds: { x: number, y: number, width: number, height: number }):
Promise<{ x: number, y: number, width: number, height: number }> {
const splashScreenOptions = this.getSplashScreenOptions();
const width = splashScreenOptions?.width ?? 640;
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved
const height = splashScreenOptions?.height ?? 480;

// determine the screen on which to show the splash screen via the center of the window to show
const windowCenterPoint = { x: initialWindowBounds.x + (initialWindowBounds.width / 2), y: initialWindowBounds.y + (initialWindowBounds.height / 2) };
const { bounds } = screen.getDisplayNearestPoint(windowCenterPoint);

// place splash screen center of screen
const screenCenterPoint = { x: bounds.x + (bounds.width / 2), y: bounds.y + (bounds.height / 2) };
const x = screenCenterPoint.x - (width / 2);
const y = screenCenterPoint.y - (height / 2);

return {
x, y, width, height
};
}

protected isShowWindowEarly(): boolean {
return !!this.config.electron.showWindowEarly &&
!('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1');
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved
}

protected showInitialWindow(): void {
if (this.config.electron.showWindowEarly &&
!('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1')) {
console.log('Showing main window early');
if (this.isShowWindowEarly() || this.isShowSplashScreen()) {
app.whenReady().then(async () => {
const options = await this.getLastWindowOptions();
// If we want to show a splash screen, don't auto open the main window
if (this.isShowSplashScreen()) {
options.preventAutomaticShow = true;
}
this.initialWindow = await this.createWindow({ ...options });
this.initialWindow.show();

if (this.isShowSplashScreen()) {
console.log('Showing splash screen');
this.configureAndShowSplashScreen(this.initialWindow);
}

// Show main window early if windows shall be shown early and splash screen is not configured
if (this.isShowWindowEarly() && !this.isShowSplashScreen()) {
console.log('Showing main window early');
this.initialWindow.show();
}
});
}
}

protected async configureAndShowSplashScreen(mainWindow: BrowserWindow): Promise<BrowserWindow> {
const splashScreenOptions = this.getSplashScreenOptions()!;
console.debug('SplashScreen options', splashScreenOptions);

const splashScreenBounds = await this.determineSplashScreenBounds(mainWindow.getBounds());
const splashScreenWindow = new BrowserWindow({
...splashScreenBounds,
frame: false,
alwaysOnTop: true,
show: false
});

if (this.isShowWindowEarly()) {
console.log('Showing splash screen early');
splashScreenWindow.show();
} else {
splashScreenWindow.on('ready-to-show', () => {
splashScreenWindow.show();
});
}

splashScreenWindow.loadFile(path.resolve(this.globals.THEIA_APP_PROJECT_PATH, splashScreenOptions.content!).toString());

// close splash screen and show main window once frontend is ready or a timeout is hit
const cancelTokenSource = new CancellationTokenSource();
const minTime = timeout(splashScreenOptions.minDuration ?? 0, cancelTokenSource.token);
const maxTime = timeout(splashScreenOptions.maxDuration ?? 30000, cancelTokenSource.token);

const showWindowAndCloseSplashScreen = () => {
cancelTokenSource.cancel();
if (!mainWindow.isVisible()) {
mainWindow.show();
}
splashScreenWindow.close();
};
TheiaRendererAPI.onApplicationStateChanged(mainWindow.webContents, state => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right event or should we just wait until the browser window has finished loading it's content?

Copy link
Member Author

@sdirix sdirix Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to wait until the frontend is ready to show real content. Otherwise the splash screen will just be shown less than 500ms before being replaced by the main window showing a loading spinner.

You can see this in the video: The splash screen is shown and the main window is shown without any loading spinner (as it's already gone)

if (state === 'ready') {
minTime.then(() => showWindowAndCloseSplashScreen());
}
});
maxTime.then(() => showWindowAndCloseSplashScreen());
return splashScreenWindow;
}

protected isShowSplashScreen(): boolean {
return typeof this.config.electron.splashScreenOptions === 'object' && !!this.config.electron.splashScreenOptions.content;
}

protected getSplashScreenOptions(): ElectronFrontendApplicationConfig.SplashScreenOptions | undefined {
if (this.isShowSplashScreen()) {
return this.config.electron.splashScreenOptions;
}
return undefined;
}

/**
* Use this rather than creating `BrowserWindow` instances from scratch, since some security parameters need to be set, this method will do it.
*
Expand All @@ -316,6 +409,7 @@ export class ElectronMainApplication {
electronWindow.window.on('focus', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus'));
this.attachSaveWindowState(electronWindow.window);
this.configureNativeSecondaryWindowCreation(electronWindow.window);

return electronWindow.window;
}

Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/electron-main/theia-electron-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export interface TheiaBrowserWindowOptions extends BrowserWindowConstructorOptio
* in which case we want to invalidate the stored options and use the default options instead.
*/
screenLayout?: string;
/**
* By default, the window will be shown as soon as the content is ready to render.
* This can be prevented by handing over preventAutomaticShow: `true`.
* Use this for fine-grained control over when to show the window, e.g. to coordinate with a splash screen.
*/
preventAutomaticShow?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to prevent the main window from showing as soon as it's ready? I don't see the use case and it makes the window state graph more complicated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not add an "open" call to TheiaElectronWindow and inoke it from the ElectronMainApplication class?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to prevent the main window from showing as soon as it's ready? I don't see the use case and it makes the window state graph more complicated.

The splash screen was intended as an alternative to the loading spinner of the main window. If we don't prevent the automatic show of the main window we will see the splash screen as well as the full main window at the same time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not add an "open" call to TheiaElectronWindow and inoke it from the ElectronMainApplication class?

Sadly, I don't understand what you mean. What is the purpose of that open and when shall it be called?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea would be that the ElectronMainApplication controls visibility of the windows instead of passing flags to the window and letting the window manage it's own visiblity.

}

export const TheiaBrowserWindowOptions = Symbol('TheiaBrowserWindowOptions');
Expand Down Expand Up @@ -76,7 +82,9 @@ export class TheiaElectronWindow {
protected init(): void {
this._window = new BrowserWindow(this.options);
this._window.setMenuBarVisibility(false);
this.attachReadyToShow();
if (!this.options.preventAutomaticShow) {
this.attachReadyToShow();
}
this.restoreMaximizedState();
this.attachCloseListeners();
this.trackApplicationState();
Expand Down
Loading