fix: using width atom for syncing app headers position (#7666)

may use global state to replace these sidebar state atoms

fix AF-1109
This commit is contained in:
pengx17
2024-07-31 07:03:30 +00:00
parent 812fdd27b5
commit 6b8f99c013
12 changed files with 118 additions and 182 deletions

View File

@@ -31,15 +31,3 @@ export const desktopTabsHeader = style({
width: '100%', width: '100%',
overflow: 'hidden', overflow: 'hidden',
}); });
export const desktopTabsHeaderTopLeft = style({
display: 'flex',
flexFlow: 'row',
alignItems: 'center',
transition: 'width 0.3s, padding 0.3s',
justifyContent: 'space-between',
marginRight: -8, // make room for tab's padding
padding: '0 16px',
flexShrink: 0,
['WebkitAppRegion' as string]: 'drag',
});

View File

@@ -35,12 +35,9 @@ import { WorkspaceAIOnboarding } from '../components/affine/ai-onboarding';
import { AppContainer } from '../components/affine/app-container'; import { AppContainer } from '../components/affine/app-container';
import { SyncAwareness } from '../components/affine/awareness'; import { SyncAwareness } from '../components/affine/awareness';
import { import {
appSidebarFloatingAtom,
appSidebarOpenAtom,
appSidebarResizingAtom, appSidebarResizingAtom,
SidebarSwitch, SidebarSwitch,
} from '../components/app-sidebar'; } from '../components/app-sidebar';
import { appSidebarWidthAtom } from '../components/app-sidebar/index.jotai';
import { AIIsland } from '../components/pure/ai-island'; import { AIIsland } from '../components/pure/ai-island';
import { RootAppSidebar } from '../components/root-app-sidebar'; import { RootAppSidebar } from '../components/root-app-sidebar';
import { MainContainer } from '../components/workspace'; import { MainContainer } from '../components/workspace';
@@ -181,29 +178,17 @@ const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
}; };
const DesktopLayout = ({ children }: PropsWithChildren) => { const DesktopLayout = ({ children }: PropsWithChildren) => {
const resizing = useAtomValue(appSidebarResizingAtom);
const sidebarWidth = useAtomValue(appSidebarWidthAtom);
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
const sidebarFloating = useAtomValue(appSidebarFloatingAtom);
const sidebarResizing = useAtomValue(appSidebarResizingAtom);
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
return ( return (
<div className={styles.desktopAppViewContainer}> <div className={styles.desktopAppViewContainer}>
<div className={styles.desktopTabsHeader}> <div className={styles.desktopTabsHeader}>
<div <AppTabsHeader
className={styles.desktopTabsHeaderTopLeft} left={
style={{ <>
transition: sidebarResizing ? 'none' : undefined,
paddingLeft:
isMacosDesktop && sidebarOpen && !sidebarFloating ? 90 : 16,
width: sidebarOpen && !sidebarFloating ? sidebarWidth : 130,
}}
>
<SidebarSwitch show /> <SidebarSwitch show />
<NavigationButtons /> <NavigationButtons />
</div> </>
<AppTabsHeader reportBoundingUpdate={!resizing} /> }
/>
</div> </div>
<div className={styles.desktopAppViewMain}> <div className={styles.desktopAppViewMain}>
<RootAppSidebar /> <RootAppSidebar />

View File

@@ -1,14 +1,20 @@
import { IconButton, Loading, observeResize } from '@affine/component'; import { IconButton, Loading } from '@affine/component';
import {
appSidebarFloatingAtom,
appSidebarOpenAtom,
appSidebarResizingAtom,
} from '@affine/core/components/app-sidebar';
import { appSidebarWidthAtom } from '@affine/core/components/app-sidebar/index.jotai';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { DesktopStateSynchronizer } from '@affine/core/modules/workbench/services/desktop-state-synchronizer'; import { DesktopStateSynchronizer } from '@affine/core/modules/workbench/services/desktop-state-synchronizer';
import type { WorkbenchMeta } from '@affine/electron-api'; import type { WorkbenchMeta } from '@affine/electron-api';
import { apis } from '@affine/electron-api'; import { apis, events } from '@affine/electron-api';
import { import {
AllDocsIcon,
CloseIcon, CloseIcon,
DeleteIcon, DeleteIcon,
EdgelessIcon, EdgelessIcon,
FolderIcon,
PageIcon, PageIcon,
PlusIcon, PlusIcon,
RightSidebarIcon, RightSidebarIcon,
@@ -21,13 +27,15 @@ import {
useService, useService,
useServiceOptional, useServiceOptional,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { debounce, partition } from 'lodash-es'; import clsx from 'clsx';
import { useAtomValue } from 'jotai';
import { partition } from 'lodash-es';
import { import {
Fragment, Fragment,
type MouseEventHandler, type MouseEventHandler,
type ReactNode, type ReactNode,
useEffect, useEffect,
useRef, useState,
} from 'react'; } from 'react';
import { import {
@@ -39,7 +47,7 @@ import * as styles from './styles.css';
type ModuleName = NonNullable<WorkbenchMeta['views'][0]['moduleName']>; type ModuleName = NonNullable<WorkbenchMeta['views'][0]['moduleName']>;
const moduleNameToIcon = { const moduleNameToIcon = {
all: <FolderIcon />, all: <AllDocsIcon />,
collection: <ViewLayersIcon />, collection: <ViewLayersIcon />,
doc: <PageIcon />, doc: <PageIcon />,
page: <PageIcon />, page: <PageIcon />,
@@ -140,13 +148,39 @@ const WorkbenchTab = ({
); );
}; };
const useIsFullScreen = () => {
const [fullScreen, setFullScreen] = useState(false);
useEffect(() => {
apis?.ui
.isFullScreen()
.then(setFullScreen)
.then(() => {
events?.ui.onFullScreen(setFullScreen);
})
.catch(console.error);
}, []);
return fullScreen;
};
export const AppTabsHeader = ({ export const AppTabsHeader = ({
style, style,
reportBoundingUpdate, mode = 'app',
className,
left,
}: { }: {
style?: React.CSSProperties; style?: React.CSSProperties;
reportBoundingUpdate?: boolean; mode?: 'app' | 'shell';
className?: string;
left?: ReactNode;
}) => { }) => {
const sidebarWidth = useAtomValue(appSidebarWidthAtom);
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
const sidebarFloating = useAtomValue(appSidebarFloatingAtom);
const sidebarResizing = useAtomValue(appSidebarResizingAtom);
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
const fullScreen = useIsFullScreen();
const tabsHeaderService = useService(AppTabsHeaderService); const tabsHeaderService = useService(AppTabsHeaderService);
const tabs = useLiveData(tabsHeaderService.tabsStatus$); const tabs = useLiveData(tabsHeaderService.tabsStatus$);
@@ -160,42 +194,36 @@ export const AppTabsHeader = ({
await tabsHeaderService.onToggleRightSidebar(); await tabsHeaderService.onToggleRightSidebar();
}, [tabsHeaderService]); }, [tabsHeaderService]);
const ref = useRef<HTMLDivElement | null>(null);
useServiceOptional(DesktopStateSynchronizer); useServiceOptional(DesktopStateSynchronizer);
useEffect(() => { useEffect(() => {
if (ref.current && reportBoundingUpdate) { if (mode === 'app') {
return observeResize( apis?.ui.pingAppLayoutReady().catch(console.error);
ref.current,
debounce(() => {
if (document.visibilityState === 'visible') {
const rect = ref.current?.getBoundingClientRect();
if (!rect) {
return;
} }
const toInt = (value: number) => Math.round(value); }, [mode]);
const boundRect = {
height: toInt(rect.height),
width: toInt(rect.width),
x: toInt(rect.x),
y: toInt(rect.y),
};
apis?.ui.updateTabsBoundingRect(boundRect).catch(console.error);
}
}, 50)
);
}
return;
}, [reportBoundingUpdate]);
return ( return (
<div <div
className={styles.root} className={clsx(styles.root, className)}
ref={ref}
style={style} style={style}
data-mode={mode}
data-is-windows={environment.isDesktop && environment.isWindows} data-is-windows={environment.isDesktop && environment.isWindows}
> >
<div
style={{
transition: sidebarResizing ? 'none' : undefined,
paddingLeft:
isMacosDesktop && sidebarOpen && !sidebarFloating && !fullScreen
? 90
: 16,
width: sidebarOpen && !sidebarFloating ? sidebarWidth : 130,
// minus 16 to account for the padding on the right side of the header (for box shadow)
marginRight: sidebarOpen && !sidebarFloating ? -16 : 0,
}}
className={styles.headerLeft}
>
{left}
</div>
<div className={styles.tabs}> <div className={styles.tabs}>
{pinned.map(tab => { {pinned.map(tab => {
return ( return (

View File

@@ -18,6 +18,20 @@ export const root = style({
}, },
}); });
export const headerLeft = style({
display: 'flex',
flexFlow: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 16px',
flexShrink: 0,
selectors: {
[`${root}[data-mode="app"] &`]: {
transition: 'width 0.3s, padding 0.3s',
},
},
});
export const tabs = style({ export const tabs = style({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',

View File

@@ -44,7 +44,6 @@ export const PageNotFound = ({
style={{ style={{
paddingLeft: environment.isMacOs ? 80 : 0, paddingLeft: environment.isMacOs ? 80 : 0,
}} }}
reportBoundingUpdate
/> />
) : null} ) : null}
{noPermission ? ( {noPermission ? (

View File

@@ -153,7 +153,6 @@ export const Component = () => {
style={{ style={{
paddingLeft: environment.isMacOs ? 80 : 0, paddingLeft: environment.isMacOs ? 80 : 0,
}} }}
reportBoundingUpdate
/> />
) : null} ) : null}
<div <div

View File

@@ -1,12 +1,13 @@
import 'setimmediate'; import 'setimmediate';
import '@affine/component/theme/global.css'; import '@affine/component/theme/global.css';
import '@affine/component/theme/theme.css'; import '@affine/component/theme/theme.css';
import '@affine/core/bootstrap/preload';
import { ThemeProvider } from '@affine/component/theme-provider'; import { ThemeProvider } from '@affine/component/theme-provider';
import { appConfigProxy } from '@affine/core/hooks/use-app-config-storage';
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header'; import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage'; import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
import { performanceLogger } from '@affine/core/shared'; import { performanceLogger } from '@affine/core/shared';
import { apis, events } from '@affine/electron-api';
import { import {
configureGlobalStorageModule, configureGlobalStorageModule,
Framework, Framework,
@@ -26,9 +27,17 @@ const frameworkProvider = framework.provider();
const logger = performanceLogger.namespace('shell'); const logger = performanceLogger.namespace('shell');
function main() { function main() {
appConfigProxy const handleMaximized = (maximized: boolean | undefined) => {
.getSync() document.documentElement.dataset.maximized = String(maximized);
.catch(() => console.error('failed to load app config')); };
const handleFullscreen = (fullscreen: boolean | undefined) => {
document.documentElement.dataset.fullscreen = String(fullscreen);
};
apis?.ui.isMaximized().then(handleMaximized).catch(console.error);
apis?.ui.isFullScreen().then(handleFullscreen).catch(console.error);
events?.ui.onMaximized(handleMaximized);
events?.ui.onFullScreen(handleFullscreen);
} }
function mountApp() { function mountApp() {

View File

@@ -1,10 +1,13 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { globalStyle, style } from '@vanilla-extract/css'; import { createVar, globalStyle, style } from '@vanilla-extract/css';
export const sidebarOffsetVar = createVar();
export const root = style({ export const root = style({
width: '100vw', width: '100vw',
height: '100vh', height: '100vh',
opacity: 1, opacity: 1,
display: 'flex',
transition: 'opacity 0.1s', transition: 'opacity 0.1s',
background: cssVar('backgroundPrimaryColor'), background: cssVar('backgroundPrimaryColor'),
selectors: { selectors: {

View File

@@ -1,12 +1,12 @@
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { AppTabsHeader } from '@affine/core/modules/app-tabs-header'; import { AppTabsHeader } from '@affine/core/modules/app-tabs-header';
import { apis, events } from '@affine/electron-api'; import { events } from '@affine/electron-api';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import * as styles from './shell.css'; import * as styles from './shell.css';
const useIsShellActive = () => { const useIsShellActive = () => {
const [active, setActive] = useState(true); const [active, setActive] = useState(false);
useEffect(() => { useEffect(() => {
const unsub = events?.ui.onTabShellViewActiveChange(active => { const unsub = events?.ui.onTabShellViewActiveChange(active => {
@@ -20,48 +20,9 @@ const useIsShellActive = () => {
return active; return active;
}; };
const useTabsBoundingRect = () => {
const [rect, setRect] = useState<{
x: number;
y: number;
width: number;
height: number;
}>({
x: environment.isDesktop && environment.isMacOs ? 80 : 0,
y: 0,
width: window.innerWidth,
height: 52,
});
useEffect(() => {
let unsub: (() => void) | undefined;
apis?.ui
.getTabsBoundingRect()
.then(rect => {
if (rect) {
setRect(rect);
}
unsub = events?.ui.onTabsBoundingRectChanged(rect => {
if (rect) {
setRect(rect);
}
});
})
.catch(err => {
console.error(err);
});
return () => {
unsub?.();
};
}, []);
return rect;
};
export function ShellRoot() { export function ShellRoot() {
const active = useIsShellActive(); const active = useIsShellActive();
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();
const rect = useTabsBoundingRect();
const translucent = const translucent =
environment.isDesktop && environment.isDesktop &&
environment.isMacOs && environment.isMacOs &&
@@ -72,15 +33,7 @@ export function ShellRoot() {
data-translucent={translucent} data-translucent={translucent}
data-active={active} data-active={active}
> >
<AppTabsHeader <AppTabsHeader mode="shell" />
style={{
position: 'fixed',
top: rect.y,
left: rect.x,
width: rect.width,
height: rect.height,
}}
/>
</div> </div>
); );
} }

View File

@@ -2,7 +2,6 @@ import type { MainEventRegister } from '../type';
import { import {
onActiveTabChanged, onActiveTabChanged,
onTabAction, onTabAction,
onTabsBoundingRectChanged,
onTabShellViewActiveChange, onTabShellViewActiveChange,
onTabsStatusChange, onTabsStatusChange,
onTabViewsMetaChanged, onTabViewsMetaChanged,
@@ -36,5 +35,4 @@ export const uiEvents = {
onTabsStatusChange, onTabsStatusChange,
onActiveTabChanged, onActiveTabChanged,
onTabShellViewActiveChange, onTabShellViewActiveChange,
onTabsBoundingRectChanged,
} satisfies Record<string, MainEventRegister>; } satisfies Record<string, MainEventRegister>;

View File

@@ -11,7 +11,6 @@ import {
closeTab, closeTab,
getMainWindow, getMainWindow,
getOnboardingWindow, getOnboardingWindow,
getTabsBoundingRect,
getTabsStatus, getTabsStatus,
getTabViewsMeta, getTabViewsMeta,
getWorkbenchMeta, getWorkbenchMeta,
@@ -19,10 +18,10 @@ import {
initAndShowMainWindow, initAndShowMainWindow,
isActiveTab, isActiveTab,
launchStage, launchStage,
pingAppLayoutReady,
showDevTools, showDevTools,
showTab, showTab,
showTabContextMenu, showTabContextMenu,
updateTabsBoundingRect,
updateWorkbenchMeta, updateWorkbenchMeta,
} from '../windows-manager'; } from '../windows-manager';
import { getChallengeResponse } from './challenge'; import { getChallengeResponse } from './challenge';
@@ -193,14 +192,8 @@ export const uiHandlers = {
uiSubjects.onToggleRightSidebar$.next(tabId); uiSubjects.onToggleRightSidebar$.next(tabId);
} }
}, },
getTabsBoundingRect: async () => { pingAppLayoutReady: async e => {
return getTabsBoundingRect(); pingAppLayoutReady(e.sender);
},
updateTabsBoundingRect: async (
e,
rect: { x: number; y: number; width: number; height: number }
) => {
return updateTabsBoundingRect(e.sender, rect);
}, },
showDevTools: async (_, ...args: Parameters<typeof showDevTools>) => { showDevTools: async (_, ...args: Parameters<typeof showDevTools>) => {
return showDevTools(...args); return showDevTools(...args);

View File

@@ -5,7 +5,6 @@ import {
type CookiesSetDetails, type CookiesSetDetails,
globalShortcut, globalShortcut,
Menu, Menu,
type Rectangle,
type View, type View,
type WebContents, type WebContents,
WebContentsView, WebContentsView,
@@ -130,7 +129,6 @@ export class WebContentViewsManager {
} }
readonly tabViewsMeta$ = TabViewsMetaState.$; readonly tabViewsMeta$ = TabViewsMetaState.$;
readonly tabsBoundingRect$ = new BehaviorSubject<Rectangle | null>(null);
readonly appTabsUIReady$ = new BehaviorSubject(new Set<string>()); readonly appTabsUIReady$ = new BehaviorSubject(new Set<string>());
// all web views // all web views
@@ -202,14 +200,6 @@ export class WebContentViewsManager {
TabViewsMetaState.patch(patch); TabViewsMetaState.patch(patch);
}; };
get tabsBoundingRect() {
return this.tabsBoundingRect$.value;
}
set tabsBoundingRect(rect: Rectangle | null) {
this.tabsBoundingRect$.next(rect);
}
get shellView() { get shellView() {
return this.webViewsMap$.value.get('shell'); return this.webViewsMap$.value.get('shell');
} }
@@ -584,14 +574,6 @@ export class WebContentViewsManager {
}) })
); );
disposables.push(
this.tabsBoundingRect$.subscribe(rect => {
if (rect) {
this.reorderViews();
}
})
);
app.on('ready', () => { app.on('ready', () => {
// bind CMD/CTRL+1~8 to switch tabs // bind CMD/CTRL+1~8 to switch tabs
// bind CMD/CTRL+9 to switch to the last tab // bind CMD/CTRL+9 to switch to the last tab
@@ -745,6 +727,11 @@ export class WebContentViewsManager {
}); });
this.resizeView(view); this.resizeView(view);
view.webContents.on('did-finish-load', () => {
this.resizeView(view);
});
// reorder will add to main window when loaded // reorder will add to main window when loaded
this.reorderViews(); this.reorderViews();
@@ -887,33 +874,13 @@ export const showDevTools = (id?: string) => {
} }
}; };
export const onTabsBoundingRectChanged = ( export const pingAppLayoutReady = (wc: WebContents) => {
fn: (rect: Rectangle | null) => void
) => {
const sub = WebContentViewsManager.instance.tabsBoundingRect$.subscribe(fn);
return () => {
sub.unsubscribe();
};
};
export const getTabsBoundingRect = () => {
return WebContentViewsManager.instance.tabsBoundingRect;
};
export const updateTabsBoundingRect = (wc: WebContents, rect: Rectangle) => {
try {
if (isActiveTab(wc)) {
WebContentViewsManager.instance.tabsBoundingRect = rect;
}
const viewId = WebContentViewsManager.instance.getViewIdFromWebContentsId( const viewId = WebContentViewsManager.instance.getViewIdFromWebContentsId(
wc.id wc.id
); );
if (viewId) { if (viewId) {
WebContentViewsManager.instance.setTabUIReady(viewId); WebContentViewsManager.instance.setTabUIReady(viewId);
} }
} catch (err) {
logger.error(err);
}
}; };
export const showTabContextMenu = async (tabId: string, viewIndex: number) => { export const showTabContextMenu = async (tabId: string, viewIndex: number) => {