From c2cf331ff760297f618723e6e7b3a9e3f90902ae Mon Sep 17 00:00:00 2001 From: pengx17 Date: Tue, 6 Aug 2024 15:57:40 +0000 Subject: [PATCH] fix(electron): fix tab view blink issue on open new tab (#7748) fix AF-1197 --- packages/frontend/core/src/pages/404.tsx | 15 ++-- packages/frontend/core/src/pages/index.tsx | 13 ++- .../electron/renderer/shell/index.css | 16 ++++ .../electron/renderer/shell/index.tsx | 5 ++ .../electron/renderer/shell/shell.css.ts | 11 +-- .../electron/renderer/shell/shell.tsx | 24 +----- packages/frontend/electron/src/main/events.ts | 18 +++-- .../src/main/windows-manager/context-menu.ts | 2 +- .../src/main/windows-manager/tab-views.ts | 81 ++++++++++++++----- tests/kit/electron.ts | 4 +- 10 files changed, 105 insertions(+), 84 deletions(-) create mode 100644 packages/frontend/electron/renderer/shell/index.css diff --git a/packages/frontend/core/src/pages/404.tsx b/packages/frontend/core/src/pages/404.tsx index 28bb0a1b21..0a8844f392 100644 --- a/packages/frontend/core/src/pages/404.tsx +++ b/packages/frontend/core/src/pages/404.tsx @@ -3,13 +3,13 @@ import { NotFoundPage, } from '@affine/component/not-found-page'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { apis } from '@affine/electron-api'; import { useLiveData, useService } from '@toeverything/infra'; import type { ReactElement } from 'react'; -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { SignOutModal } from '../components/affine/sign-out-modal'; import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper'; -import { AppTabsHeader } from '../modules/app-tabs-header'; import { AuthService } from '../modules/cloud'; import { SignIn } from './sign-in'; @@ -37,15 +37,12 @@ export const PageNotFound = ({ await authService.signOut(); }, [authService]); + useEffect(() => { + apis?.ui.pingAppLayoutReady().catch(console.error); + }, []); + return ( <> - {environment.isDesktop ? ( - - ) : null} {noPermission ? ( { navigating, ]); + useEffect(() => { + apis?.ui.pingAppLayoutReady().catch(console.error); + }, []); + useEffect(() => { setCreating(true); createFirstAppData(workspacesService) @@ -148,13 +152,6 @@ export const Component = () => { // TODO(@eyhn): We need a no workspace page return ( <> - {environment.isDesktop ? ( - - ) : null}
{ document.documentElement.dataset.fullscreen = String(fullscreen); }; + const handleActive = (active: boolean | undefined) => { + document.documentElement.dataset.active = String(active); + }; apis?.ui.isMaximized().then(handleMaximized).catch(console.error); apis?.ui.isFullScreen().then(handleFullscreen).catch(console.error); events?.ui.onMaximized(handleMaximized); events?.ui.onFullScreen(handleFullscreen); + events?.ui.onTabShellViewActiveChange(handleActive); await loadLanguage(); mountApp(); diff --git a/packages/frontend/electron/renderer/shell/shell.css.ts b/packages/frontend/electron/renderer/shell/shell.css.ts index 6e1286f565..44a8872602 100644 --- a/packages/frontend/electron/renderer/shell/shell.css.ts +++ b/packages/frontend/electron/renderer/shell/shell.css.ts @@ -1,25 +1,16 @@ import { cssVar } from '@toeverything/theme'; -import { createVar, globalStyle, style } from '@vanilla-extract/css'; +import { createVar, style } from '@vanilla-extract/css'; export const sidebarOffsetVar = createVar(); export const root = style({ width: '100vw', height: '100vh', - opacity: 1, display: 'flex', - transition: 'opacity 0.1s', background: cssVar('backgroundPrimaryColor'), selectors: { - '&[data-active="false"]': { - opacity: 0, - }, '&[data-translucent="true"]': { background: 'transparent', }, }, }); - -globalStyle(`${root}[data-active="false"] *`, { - ['WebkitAppRegion' as string]: 'no-drag !important', -}); diff --git a/packages/frontend/electron/renderer/shell/shell.tsx b/packages/frontend/electron/renderer/shell/shell.tsx index 324294d949..04dced50aa 100644 --- a/packages/frontend/electron/renderer/shell/shell.tsx +++ b/packages/frontend/electron/renderer/shell/shell.tsx @@ -1,38 +1,16 @@ import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { AppTabsHeader } from '@affine/core/modules/app-tabs-header'; -import { events } from '@affine/electron-api'; -import { useEffect, useState } from 'react'; import * as styles from './shell.css'; -const useIsShellActive = () => { - const [active, setActive] = useState(false); - - useEffect(() => { - const unsub = events?.ui.onTabShellViewActiveChange(active => { - setActive(active); - }); - return () => { - unsub?.(); - }; - }); - - return active; -}; - export function ShellRoot() { - const active = useIsShellActive(); const { appSettings } = useAppSettingHelper(); const translucent = environment.isDesktop && environment.isMacOs && appSettings.enableBlurBackground; return ( -
+
); diff --git a/packages/frontend/electron/src/main/events.ts b/packages/frontend/electron/src/main/events.ts index a2bea51af3..07064aa4d9 100644 --- a/packages/frontend/electron/src/main/events.ts +++ b/packages/frontend/electron/src/main/events.ts @@ -19,6 +19,7 @@ function getActiveWindows() { } export function registerEvents() { + const unsubs: (() => void)[] = []; // register events for (const [namespace, namespaceEvents] of Object.entries(allEvents)) { for (const [key, eventRegister] of Object.entries(namespaceEvents)) { @@ -52,14 +53,15 @@ export function registerEvents() { }); }); }); - app.on('before-quit', () => { - // subscription on quit sometimes crashes the app - try { - unsubscribe(); - } catch (err) { - logger.error('unsubscribe error', err); - } - }); + unsubs.push(unsubscribe); } } + app.on('before-quit', () => { + // subscription on quit sometimes crashes the app + try { + unsubs.forEach(unsub => unsub()); + } catch (err) { + logger.error('unsubscribe error', err); + } + }); } diff --git a/packages/frontend/electron/src/main/windows-manager/context-menu.ts b/packages/frontend/electron/src/main/windows-manager/context-menu.ts index 5cb88d4e92..94f1c9612f 100644 --- a/packages/frontend/electron/src/main/windows-manager/context-menu.ts +++ b/packages/frontend/electron/src/main/windows-manager/context-menu.ts @@ -62,7 +62,7 @@ export const showTabContextMenu = async (tabId: string, viewIndex: number) => { }, }, - ...(workbenches.length > 0 + ...(workbenches.length > 1 ? ([ { type: 'separator' }, { diff --git a/packages/frontend/electron/src/main/windows-manager/tab-views.ts b/packages/frontend/electron/src/main/windows-manager/tab-views.ts index 9e92770ef4..6a1365e913 100644 --- a/packages/frontend/electron/src/main/windows-manager/tab-views.ts +++ b/packages/frontend/electron/src/main/windows-manager/tab-views.ts @@ -140,6 +140,10 @@ export class WebContentViewsManager { readonly tabViewsMeta$ = TabViewsMetaState.$; readonly appTabsUIReady$ = new BehaviorSubject(new Set()); + get appTabsUIReady() { + return this.appTabsUIReady$.value; + } + // all web views readonly webViewsMap$ = new BehaviorSubject( new Map() @@ -251,8 +255,12 @@ export class WebContentViewsManager { } setTabUIReady = (tabId: string) => { - this.appTabsUIReady$.next(new Set([...this.appTabsUIReady$.value, tabId])); + this.appTabsUIReady$.next(new Set([...this.appTabsUIReady, tabId])); this.reorderViews(); + const view = this.tabViewsMap.get(tabId); + if (view) { + this.resizeView(view); + } }; getViewIdFromWebContentsId = (id: number) => { @@ -460,10 +468,6 @@ export class WebContentViewsManager { showTab = async (id: string): Promise => { if (this.activeWorkbenchId !== id) { - // todo: this will cause the shell view to be on top and flickers the screen - // this.appTabsUIReady$.next( - // new Set([...this.appTabsUIReady$.value].filter(key => key !== id)) - // ); this.patchTabViewsMeta({ activeWorkbenchId: id, }); @@ -601,26 +605,59 @@ export class WebContentViewsManager { // if tab ui of the current active view is not ready, // make sure shell view is on top const activeView = this.activeWorkbenchView; - const ready = this.activeWorkbenchId - ? this.appTabsUIReady$.value.has(this.activeWorkbenchId) - : false; - // inactive < active view (not ready) < shell < active view (ready) - const getScore = (view: View) => { - if (view === this.shellView) { - return 2; - } - if (view === activeView) { - return ready ? 3 : 1; - } - return 0; + const getViewId = (view: View) => { + return [...this.tabViewsMap.entries()].find( + ([_, v]) => v === view + )?.[0]; }; - [...this.tabViewsMap.values()] - .toSorted((a, b) => getScore(a) - getScore(b)) - .forEach((view, index) => { - this.mainWindow?.contentView.addChildView(view, index); - }); + const isViewReady = (view: View) => { + if (view === this.shellView) { + return true; + } + const id = getViewId(view); + return id ? this.appTabsUIReady.has(id) : false; + }; + + // 2: active view (ready) + // 1: shell + // 0: inactive view (ready) + // -1 inactive view (not ready) + // -1 active view (not ready) + const getScore = (view: View) => { + if (view === this.shellView) { + return 1; + } + const viewReady = isViewReady(view); + if (view === activeView) { + return viewReady ? 2 : -1; + } else { + return viewReady ? 0 : -1; + } + }; + + const sorted = [...this.tabViewsMap.entries()] + .map(([id, view]) => { + return { + id, + view, + score: getScore(view), + }; + }) + .filter(({ score }) => score >= 0) + .toSorted((a, b) => a.score - b.score); + + // remove inactive views + this.mainWindow?.contentView.children.forEach(view => { + if (!isViewReady(view)) { + this.mainWindow?.contentView.removeChildView(view); + } + }); + + sorted.forEach(({ view }, idx) => { + this.mainWindow?.contentView.addChildView(view, idx); + }); } }; diff --git a/tests/kit/electron.ts b/tests/kit/electron.ts index 1707cfee78..2e07733ab9 100644 --- a/tests/kit/electron.ts +++ b/tests/kit/electron.ts @@ -140,10 +140,8 @@ export const test = base.extend<{ await use(electronApp); console.log('Cleaning up...'); const pages = electronApp.windows(); - for (let i = 0; i < pages.length; i++) { - const page = pages[i]; + for (const page of pages) { await page.close(); - console.log(`Closed page ${i + 1}/${pages.length}`); } await electronApp.close(); await removeWithRetry(clonedDist);