Ensure overlay modal grabs input

This commit is contained in:
2026-02-23 19:54:58 -08:00
parent fe8a71990a
commit 978cb8c401
24 changed files with 562 additions and 63 deletions

View File

@@ -0,0 +1,218 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createOverlayModalRuntimeService } from './overlay-runtime';
type MockWindow = {
destroyed: boolean;
visible: boolean;
focused: boolean;
ignoreMouseEvents: boolean;
webContentsFocused: boolean;
showCount: number;
hideCount: number;
sent: unknown[][];
loading: boolean;
loadCallbacks: Array<() => void>;
};
function createMockWindow(): MockWindow & {
isDestroyed: () => boolean;
isVisible: () => boolean;
isFocused: () => boolean;
setIgnoreMouseEvents: (ignore: boolean) => void;
getShowCount: () => number;
getHideCount: () => number;
show: () => void;
hide: () => void;
focus: () => void;
webContents: {
focused: boolean;
isLoading: () => boolean;
send: (channel: string, payload?: unknown) => void;
isFocused: () => boolean;
once: (event: 'did-finish-load', cb: () => void) => void;
focus: () => void;
};
} {
const state: MockWindow = {
destroyed: false,
visible: false,
focused: false,
ignoreMouseEvents: false,
webContentsFocused: false,
showCount: 0,
hideCount: 0,
sent: [],
loading: false,
loadCallbacks: [],
};
return {
...state,
isDestroyed: () => state.destroyed,
isVisible: () => state.visible,
isFocused: () => state.focused,
setIgnoreMouseEvents: (ignore: boolean) => {
state.ignoreMouseEvents = ignore;
},
getShowCount: () => state.showCount,
getHideCount: () => state.hideCount,
show: () => {
state.visible = true;
state.showCount += 1;
},
hide: () => {
state.visible = false;
state.hideCount += 1;
},
focus: () => {
state.focused = true;
},
webContents: {
isLoading: () => state.loading,
send: (channel, payload) => {
if (payload === undefined) {
state.sent.push([channel]);
return;
}
state.sent.push([channel, payload]);
},
focused: false,
isFocused: () => state.webContentsFocused,
once: (_event, cb) => {
state.loadCallbacks.push(cb);
},
focus: () => {
state.webContentsFocused = true;
},
},
};
}
test('sendToActiveOverlayWindow targets modal window with full geometry and tracks close restore', () => {
const window = createMockWindow();
const calls: string[] = [];
const runtime = createOverlayModalRuntimeService({
getMainWindow: () => null,
getInvisibleWindow: () => null,
getModalWindow: () => window as never,
createModalWindow: () => {
calls.push('create-modal-window');
return window as never;
},
getModalGeometry: () => ({ x: 10, y: 20, width: 300, height: 200 }),
setModalWindowBounds: (geometry) => {
calls.push(`bounds:${geometry.x},${geometry.y},${geometry.width},${geometry.height}`);
},
});
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
restoreOnModalClose: 'runtime-options',
});
assert.equal(sent, true);
assert.equal(runtime.getRestoreVisibleOverlayOnModalClose().has('runtime-options'), true);
assert.deepEqual(calls, ['bounds:10,20,300,200']);
assert.equal(window.getShowCount(), 1);
assert.equal(window.isFocused(), true);
assert.deepEqual(window.sent, [['runtime-options:open']]);
});
test('sendToActiveOverlayWindow creates modal window lazily when absent', () => {
const window = createMockWindow();
let modalWindow: ReturnType<typeof createMockWindow> | null = null;
const runtime = createOverlayModalRuntimeService({
getMainWindow: () => null,
getInvisibleWindow: () => null,
getModalWindow: () => modalWindow as never,
createModalWindow: () => {
modalWindow = window;
return modalWindow as never;
},
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
});
assert.equal(
runtime.sendToActiveOverlayWindow('jimaku:open', undefined, { restoreOnModalClose: 'jimaku' }),
true,
);
assert.deepEqual(window.sent, [['jimaku:open']]);
});
test('handleOverlayModalClosed hides modal window only after all pending modals close', () => {
const window = createMockWindow();
const runtime = createOverlayModalRuntimeService({
getMainWindow: () => null,
getInvisibleWindow: () => null,
getModalWindow: () => window as never,
createModalWindow: () => window as never,
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
});
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
restoreOnModalClose: 'runtime-options',
});
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, {
restoreOnModalClose: 'subsync',
});
runtime.handleOverlayModalClosed('runtime-options');
assert.equal(window.getHideCount(), 0);
runtime.handleOverlayModalClosed('subsync');
assert.equal(window.getHideCount(), 1);
});
test('modal runtime notifies callers when modal input state becomes active/inactive', () => {
const window = createMockWindow();
const state: boolean[] = [];
const runtime = createOverlayModalRuntimeService(
{
getMainWindow: () => null,
getInvisibleWindow: () => null,
getModalWindow: () => window as never,
createModalWindow: () => window as never,
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
},
{
onModalStateChange: (active: boolean): void => {
state.push(active);
},
},
);
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
restoreOnModalClose: 'runtime-options',
});
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, {
restoreOnModalClose: 'subsync',
});
assert.deepEqual(state, [true]);
runtime.handleOverlayModalClosed('runtime-options');
assert.deepEqual(state, [true]);
runtime.handleOverlayModalClosed('subsync');
assert.deepEqual(state, [true, false]);
});
test('handleOverlayModalClosed hides modal window for single kiku modal', () => {
const window = createMockWindow();
const runtime = createOverlayModalRuntimeService({
getMainWindow: () => null,
getInvisibleWindow: () => null,
getModalWindow: () => window as never,
createModalWindow: () => window as never,
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
});
runtime.sendToActiveOverlayWindow('kiku:field-grouping-open', { test: true }, {
restoreOnModalClose: 'kiku',
});
runtime.handleOverlayModalClosed('kiku');
assert.equal(window.getHideCount(), 1);
assert.equal(runtime.getRestoreVisibleOverlayOnModalClose().size, 0);
});

View File

@@ -1,11 +1,16 @@
import type { BrowserWindow } from 'electron';
import type { WindowGeometry } from '../types';
type OverlayHostedModal = 'runtime-options' | 'subsync' | 'jimaku';
type OverlayHostedModal = 'runtime-options' | 'subsync' | 'jimaku' | 'kiku';
type OverlayHostLayer = 'visible' | 'invisible';
export interface OverlayWindowResolver {
getMainWindow: () => BrowserWindow | null;
getInvisibleWindow: () => BrowserWindow | null;
getModalWindow: () => BrowserWindow | null;
createModalWindow: () => BrowserWindow | null;
getModalGeometry: () => WindowGeometry;
setModalWindowBounds: (geometry: WindowGeometry) => void;
}
export interface OverlayModalRuntime {
@@ -19,9 +24,34 @@ export interface OverlayModalRuntime {
getRestoreVisibleOverlayOnModalClose: () => Set<OverlayHostedModal>;
}
export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): OverlayModalRuntime {
export interface OverlayModalRuntimeOptions {
onModalStateChange?: (isActive: boolean) => void;
}
export function createOverlayModalRuntimeService(
deps: OverlayWindowResolver,
options: OverlayModalRuntimeOptions = {},
): OverlayModalRuntime {
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
const overlayModalAutoShownLayer = new Map<OverlayHostedModal, OverlayHostLayer>();
let modalActive = false;
const notifyModalStateChange = (nextState: boolean): void => {
if (modalActive === nextState) return;
modalActive = nextState;
options.onModalStateChange?.(nextState);
};
const resolveModalWindow = (): BrowserWindow | null => {
const existingWindow = deps.getModalWindow();
if (existingWindow && !existingWindow.isDestroyed()) {
return existingWindow;
}
const createdWindow = deps.createModalWindow();
if (!createdWindow || createdWindow.isDestroyed()) {
return null;
}
return createdWindow;
};
const getTargetOverlayWindow = (): {
window: BrowserWindow;
@@ -41,6 +71,15 @@ export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): O
return null;
};
const showModalWindow = (window: BrowserWindow): void => {
window.show();
window.setIgnoreMouseEvents(false);
window.focus();
if (!window.webContents.isFocused()) {
window.webContents.focus();
}
};
const showOverlayWindowForModal = (window: BrowserWindow, layer: OverlayHostLayer): void => {
if (layer === 'invisible' && typeof window.showInactive === 'function') {
window.showInactive();
@@ -57,39 +96,66 @@ export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): O
payload?: unknown,
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
): boolean => {
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
const sendNow = (window: BrowserWindow): void => {
if (payload === undefined) {
window.webContents.send(channel);
} else {
window.webContents.send(channel, payload);
}
};
if (restoreOnModalClose) {
const modalWindow = resolveModalWindow();
if (!modalWindow) return false;
deps.setModalWindowBounds(deps.getModalGeometry());
const wasVisible = modalWindow.isVisible();
const wasModalActive = restoreVisibleOverlayOnModalClose.size > 0;
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
if (!wasModalActive) {
notifyModalStateChange(true);
}
if (!wasVisible) {
showModalWindow(modalWindow);
} else if (!modalWindow.isFocused()) {
showModalWindow(modalWindow);
}
if (modalWindow.webContents.isLoading()) {
modalWindow.webContents.once('did-finish-load', () => {
if (modalWindow && !modalWindow.isDestroyed() && !modalWindow.webContents.isLoading()) {
sendNow(modalWindow);
}
});
return true;
}
sendNow(modalWindow);
return true;
}
const target = getTargetOverlayWindow();
if (!target) return false;
const { window: targetWindow, layer } = target;
const wasVisible = targetWindow.isVisible();
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
const sendNow = (): void => {
if (payload === undefined) {
targetWindow.webContents.send(channel);
} else {
targetWindow.webContents.send(channel, payload);
}
};
if (!wasVisible) {
showOverlayWindowForModal(targetWindow, layer);
}
if (!wasVisible && restoreOnModalClose) {
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
overlayModalAutoShownLayer.set(restoreOnModalClose, layer);
}
if (targetWindow.webContents.isLoading()) {
targetWindow.webContents.once('did-finish-load', () => {
if (targetWindow && !targetWindow.isDestroyed() && !targetWindow.webContents.isLoading()) {
sendNow();
sendNow(targetWindow);
}
});
return true;
}
sendNow();
sendNow(targetWindow);
return true;
};
@@ -102,24 +168,13 @@ export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): O
const handleOverlayModalClosed = (modal: OverlayHostedModal): void => {
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
restoreVisibleOverlayOnModalClose.delete(modal);
const layer = overlayModalAutoShownLayer.get(modal);
overlayModalAutoShownLayer.delete(modal);
if (!layer) return;
const shouldKeepLayerVisible = [...restoreVisibleOverlayOnModalClose].some(
(pendingModal) => overlayModalAutoShownLayer.get(pendingModal) === layer,
);
if (shouldKeepLayerVisible) return;
if (layer === 'visible') {
const mainWindow = deps.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.hide();
}
return;
const modalWindow = deps.getModalWindow();
if (!modalWindow || modalWindow.isDestroyed()) return;
if (restoreVisibleOverlayOnModalClose.size === 0) {
notifyModalStateChange(false);
}
const invisibleWindow = deps.getInvisibleWindow();
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
invisibleWindow.hide();
if (restoreVisibleOverlayOnModalClose.size === 0) {
modalWindow.hide();
}
};

View File

@@ -20,11 +20,23 @@ test('overlay content measurement store main deps builder maps callbacks', () =>
test('overlay modal runtime main deps builder maps window resolvers', () => {
const mainWindow = { id: 'main' };
const invisibleWindow = { id: 'invisible' };
const modalWindow = { id: 'modal' };
const calls: string[] = [];
const deps = createBuildOverlayModalRuntimeMainDepsHandler({
getMainWindow: () => mainWindow as never,
getInvisibleWindow: () => invisibleWindow as never,
getModalWindow: () => modalWindow as never,
createModalWindow: () => modalWindow as never,
getModalGeometry: () => ({ x: 1, y: 2, width: 3, height: 4 }),
setModalWindowBounds: (geometry) =>
calls.push(`modal-bounds:${geometry.x},${geometry.y},${geometry.width},${geometry.height}`),
})();
assert.equal(deps.getMainWindow(), mainWindow);
assert.equal(deps.getInvisibleWindow(), invisibleWindow);
assert.equal(deps.getModalWindow(), modalWindow);
assert.equal(deps.createModalWindow(), modalWindow);
assert.deepEqual(deps.getModalGeometry(), { x: 1, y: 2, width: 3, height: 4 });
deps.setModalWindowBounds({ x: 10, y: 20, width: 30, height: 40 });
assert.deepEqual(calls, ['modal-bounds:10,20,30,40']);
});

View File

@@ -14,9 +14,15 @@ export function createBuildOverlayContentMeasurementStoreMainDepsHandler(
});
}
export function createBuildOverlayModalRuntimeMainDepsHandler(deps: OverlayWindowResolver) {
export function createBuildOverlayModalRuntimeMainDepsHandler(
deps: OverlayWindowResolver,
) {
return (): OverlayWindowResolver => ({
getMainWindow: () => deps.getMainWindow(),
getInvisibleWindow: () => deps.getInvisibleWindow(),
getModalWindow: () => deps.getModalWindow(),
createModalWindow: () => deps.createModalWindow(),
getModalGeometry: () => deps.getModalGeometry(),
setModalWindowBounds: (geometry) => deps.setModalWindowBounds(geometry),
});
}

View File

@@ -3,6 +3,7 @@ import test from 'node:test';
import {
createBuildCreateInvisibleWindowMainDepsHandler,
createBuildCreateMainWindowMainDepsHandler,
createBuildCreateModalWindowMainDepsHandler,
createBuildCreateOverlayWindowMainDepsHandler,
createBuildCreateSecondaryWindowMainDepsHandler,
} from './overlay-window-factory-main-deps';
@@ -47,5 +48,12 @@ test('overlay window factory main deps builders return mapped handlers', () => {
const secondaryDeps = buildSecondaryDeps();
secondaryDeps.setSecondaryWindow(null);
assert.deepEqual(calls, ['set-main', 'set-invisible', 'set-secondary']);
const buildModalDeps = createBuildCreateModalWindowMainDepsHandler({
createOverlayWindow: () => ({ id: 'modal' }),
setModalWindow: () => calls.push('set-modal'),
});
const modalDeps = buildModalDeps();
modalDeps.setModalWindow(null);
assert.deepEqual(calls, ['set-main', 'set-invisible', 'set-secondary', 'set-modal']);
});

View File

@@ -1,15 +1,15 @@
export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
createOverlayWindowCore: (
kind: 'visible' | 'invisible' | 'secondary',
kind: 'visible' | 'invisible' | 'secondary' | 'modal',
options: {
isDev: boolean;
overlayDebugVisualizationEnabled: boolean;
ensureOverlayWindowLevel: (window: TWindow) => void;
onRuntimeOptionsChanged: () => void;
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary') => boolean;
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => boolean;
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary') => void;
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => void;
},
) => TWindow;
isDev: boolean;
@@ -17,9 +17,9 @@ export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
ensureOverlayWindowLevel: (window: TWindow) => void;
onRuntimeOptionsChanged: () => void;
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary') => boolean;
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => boolean;
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary') => void;
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => void;
}) {
return () => ({
createOverlayWindowCore: deps.createOverlayWindowCore,
@@ -35,7 +35,7 @@ export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
}
export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
setMainWindow: (window: TWindow | null) => void;
}) {
return () => ({
@@ -45,7 +45,7 @@ export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
}
export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
setInvisibleWindow: (window: TWindow | null) => void;
}) {
return () => ({
@@ -55,7 +55,7 @@ export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
}
export function createBuildCreateSecondaryWindowMainDepsHandler<TWindow>(deps: {
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
setSecondaryWindow: (window: TWindow | null) => void;
}) {
return () => ({
@@ -63,3 +63,13 @@ export function createBuildCreateSecondaryWindowMainDepsHandler<TWindow>(deps: {
setSecondaryWindow: deps.setSecondaryWindow,
});
}
export function createBuildCreateModalWindowMainDepsHandler<TWindow>(deps: {
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
setModalWindow: (window: TWindow | null) => void;
}) {
return () => ({
createOverlayWindow: deps.createOverlayWindow,
setModalWindow: deps.setModalWindow,
});
}

View File

@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import {
createCreateInvisibleWindowHandler,
createCreateMainWindowHandler,
createCreateModalWindowHandler,
createCreateOverlayWindowHandler,
createCreateSecondaryWindowHandler,
} from './overlay-window-factory';
@@ -80,3 +81,18 @@ test('create secondary window handler stores secondary window', () => {
assert.equal(createSecondaryWindow(), secondaryWindow);
assert.deepEqual(calls, ['create:secondary', 'set:secondary']);
});
test('create modal window handler stores modal window', () => {
const calls: string[] = [];
const modalWindow = { id: 'modal' };
const createModalWindow = createCreateModalWindowHandler({
createOverlayWindow: (kind) => {
calls.push(`create:${kind}`);
return modalWindow;
},
setModalWindow: (window) => calls.push(`set:${(window as { id: string }).id}`),
});
assert.equal(createModalWindow(), modalWindow);
assert.deepEqual(calls, ['create:modal', 'set:modal']);
});

View File

@@ -1,4 +1,4 @@
type OverlayWindowKind = 'visible' | 'invisible' | 'secondary';
type OverlayWindowKind = 'visible' | 'invisible' | 'secondary' | 'modal';
export function createCreateOverlayWindowHandler<TWindow>(deps: {
createOverlayWindowCore: (
@@ -69,3 +69,14 @@ export function createCreateSecondaryWindowHandler<TWindow>(deps: {
return window;
};
}
export function createCreateModalWindowHandler<TWindow>(deps: {
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
setModalWindow: (window: TWindow | null) => void;
}) {
return (): TWindow => {
const window = deps.createOverlayWindow('modal');
deps.setModalWindow(window);
return window;
};
}

View File

@@ -6,6 +6,7 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
let mainWindow: { kind: string } | null = null;
let invisibleWindow: { kind: string } | null = null;
let secondaryWindow: { kind: string } | null = null;
let modalWindow: { kind: string } | null = null;
let debugEnabled = false;
const calls: string[] = [];
@@ -32,6 +33,9 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
setSecondaryWindow: (window) => {
secondaryWindow = window;
},
setModalWindow: (window) => {
modalWindow = window;
},
});
assert.deepEqual(runtime.createOverlayWindow('visible'), { kind: 'visible' });
@@ -46,6 +50,8 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
assert.deepEqual(runtime.createSecondaryWindow(), { kind: 'secondary' });
assert.deepEqual(secondaryWindow, { kind: 'secondary' });
assert.deepEqual(runtime.createModalWindow(), { kind: 'modal' });
assert.deepEqual(modalWindow, { kind: 'modal' });
assert.equal(debugEnabled, false);
assert.deepEqual(calls, []);

View File

@@ -1,12 +1,14 @@
import {
createCreateInvisibleWindowHandler,
createCreateMainWindowHandler,
createCreateModalWindowHandler,
createCreateOverlayWindowHandler,
createCreateSecondaryWindowHandler,
} from './overlay-window-factory';
import {
createBuildCreateInvisibleWindowMainDepsHandler,
createBuildCreateMainWindowMainDepsHandler,
createBuildCreateModalWindowMainDepsHandler,
createBuildCreateOverlayWindowMainDepsHandler,
createBuildCreateSecondaryWindowMainDepsHandler,
} from './overlay-window-factory-main-deps';
@@ -20,6 +22,7 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
setMainWindow: (window: TWindow | null) => void;
setInvisibleWindow: (window: TWindow | null) => void;
setSecondaryWindow: (window: TWindow | null) => void;
setModalWindow: (window: TWindow | null) => void;
}) {
const createOverlayWindow = createCreateOverlayWindowHandler<TWindow>(
createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps.createOverlayWindowDeps)(),
@@ -42,11 +45,18 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
setSecondaryWindow: (window) => deps.setSecondaryWindow(window),
})(),
);
const createModalWindow = createCreateModalWindowHandler<TWindow>(
createBuildCreateModalWindowMainDepsHandler<TWindow>({
createOverlayWindow: (kind) => createOverlayWindow(kind),
setModalWindow: (window) => deps.setModalWindow(window),
})(),
);
return {
createOverlayWindow,
createMainWindow,
createInvisibleWindow,
createSecondaryWindow,
createModalWindow,
};
}