feat: add scroll wheel zoom setting (#9476)

### Changed
Add `scroll wheel to zoom` setting option, when the option enables, user can zoom in and out with scroll wheel without pressing the cmd/ctrl key.
This commit is contained in:
doouding
2025-01-03 06:09:10 +00:00
parent 0699205721
commit cb5d7eaabc
13 changed files with 155 additions and 36 deletions
@@ -2,13 +2,17 @@ import type { ExtensionType } from '@blocksuite/block-std';
import { createIdentifier } from '@blocksuite/global/di'; import { createIdentifier } from '@blocksuite/global/di';
import type { DeepPartial } from '@blocksuite/global/utils'; import type { DeepPartial } from '@blocksuite/global/utils';
import type { Signal } from '@preact/signals-core'; import type { Signal } from '@preact/signals-core';
import type { z } from 'zod'; import { z } from 'zod';
import { NodePropsSchema } from '../utils/index.js'; import { NodePropsSchema } from '../utils/index.js';
export const EditorSettingSchema = NodePropsSchema; export const GeneralSettingSchema = z
.object({
edgelessScrollZoom: z.boolean().default(false),
})
.merge(NodePropsSchema);
export type EditorSetting = z.infer<typeof EditorSettingSchema>; export type EditorSetting = z.infer<typeof GeneralSettingSchema>;
export const EditorSettingProvider = createIdentifier< export const EditorSettingProvider = createIdentifier<
Signal<DeepPartial<EditorSetting>> Signal<DeepPartial<EditorSetting>>
@@ -11,6 +11,7 @@ import type {
ShapeElementModel, ShapeElementModel,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
EditorSettingProvider,
EditPropsStore, EditPropsStore,
FontLoaderService, FontLoaderService,
ThemeProvider, ThemeProvider,
@@ -371,8 +372,10 @@ export class EdgelessRootBlockComponent extends BlockComponent<
private _initWheelEvent() { private _initWheelEvent() {
this._disposables.add( this._disposables.add(
this.dispatcher.add('wheel', ctx => { this.dispatcher.add('wheel', ctx => {
const config = this.std.getOptional(EditorSettingProvider);
const state = ctx.get('defaultState'); const state = ctx.get('defaultState');
const e = state.event as WheelEvent; const e = state.event as WheelEvent;
const edgelessScrollZoom = config?.peek().edgelessScrollZoom ?? false;
e.preventDefault(); e.preventDefault();
@@ -380,7 +383,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
if (viewport.locked) return; if (viewport.locked) return;
// zoom // zoom
if (isTouchPadPinchEvent(e)) { if (isTouchPadPinchEvent(e) || edgelessScrollZoom) {
const rect = this.getBoundingClientRect(); const rect = this.getBoundingClientRect();
// Perform zooming relative to the mouse position // Perform zooming relative to the mouse position
const [baseX, baseY] = this.gfx.viewport.toModelCoord( const [baseX, baseY] = this.gfx.viewport.toModelCoord(
@@ -3,11 +3,13 @@ import type {
PeekViewService, PeekViewService,
} from '@blocksuite/affine-components/peek'; } from '@blocksuite/affine-components/peek';
import { PeekViewExtension } from '@blocksuite/affine-components/peek'; import { PeekViewExtension } from '@blocksuite/affine-components/peek';
import type { EditorSetting } from '@blocksuite/affine-shared/services';
import { BlockComponent } from '@blocksuite/block-std'; import { BlockComponent } from '@blocksuite/block-std';
import { import {
ColorScheme, ColorScheme,
type DocMode, type DocMode,
type DocModeProvider, type DocModeProvider,
GeneralSettingSchema,
type GenerateDocUrlService, type GenerateDocUrlService,
matchFlavours, matchFlavours,
type NotificationService, type NotificationService,
@@ -19,7 +21,7 @@ import {
import { Slot } from '@blocksuite/global/utils'; import { Slot } from '@blocksuite/global/utils';
import type { AffineEditorContainer } from '@blocksuite/presets'; import type { AffineEditorContainer } from '@blocksuite/presets';
import { type DocCollection } from '@blocksuite/store'; import { type DocCollection } from '@blocksuite/store';
import { signal } from '@preact/signals-core'; import { Signal, signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit'; import type { TemplateResult } from 'lit';
import type { AttachmentViewerPanel } from './components/attachment-viewer-panel.js'; import type { AttachmentViewerPanel } from './components/attachment-viewer-panel.js';
@@ -204,3 +206,28 @@ export function mockGenerateDocUrlService(collection: DocCollection) {
}; };
return generateDocUrlService; return generateDocUrlService;
} }
export function mockEditorSetting() {
if (window.editorSetting$) return window.editorSetting$;
const initialVal = Object.entries(GeneralSettingSchema.shape).reduce(
(pre: EditorSetting, [key, schema]) => {
// @ts-expect-error key is EditorSetting field
pre[key as keyof EditorSetting] = schema.parse(undefined);
return pre;
},
{} as EditorSetting
);
const signal = new Signal<EditorSetting>(initialVal);
window.editorSetting$ = signal;
return signal;
}
declare global {
interface Window {
editorSetting$: Signal<EditorSetting>;
}
}
@@ -3,6 +3,7 @@ import {
CommunityCanvasTextFonts, CommunityCanvasTextFonts,
DocModeExtension, DocModeExtension,
DocModeProvider, DocModeProvider,
EditorSettingExtension,
FontConfigExtension, FontConfigExtension,
GenerateDocUrlExtension, GenerateDocUrlExtension,
GenerateDocUrlProvider, GenerateDocUrlProvider,
@@ -26,6 +27,7 @@ import {
} from '../../_common/history.js'; } from '../../_common/history.js';
import { import {
mockDocModeService, mockDocModeService,
mockEditorSetting,
mockGenerateDocUrlService, mockGenerateDocUrlService,
mockNotificationService, mockNotificationService,
mockParseDocUrlService, mockParseDocUrlService,
@@ -126,6 +128,7 @@ export async function mountDefaultDocEditor(collection: DocCollection) {
NotificationExtension(mockNotificationService(editor)), NotificationExtension(mockNotificationService(editor)),
FontConfigExtension(CommunityCanvasTextFonts), FontConfigExtension(CommunityCanvasTextFonts),
mockPeekViewExtension(attachmentViewerPanel), mockPeekViewExtension(attachmentViewerPanel),
EditorSettingExtension(mockEditorSetting()),
]; ];
return newSpec; return newSpec;
} }
+5 -1
View File
@@ -22,7 +22,10 @@ import { effects as presetsEffects } from '@blocksuite/presets/effects';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import * as store from '@blocksuite/store'; import * as store from '@blocksuite/store';
import { mockDocModeService } from '../_common/mock-services.js'; import {
mockDocModeService,
mockEditorSetting,
} from '../_common/mock-services.js';
import { setupEdgelessTemplate } from '../_common/setup.js'; import { setupEdgelessTemplate } from '../_common/setup.js';
import { import {
createStarterDocCollection, createStarterDocCollection,
@@ -59,6 +62,7 @@ async function main() {
}, },
defaultExtensions: (): ExtensionType[] => [ defaultExtensions: (): ExtensionType[] => [
FontConfigExtension(CommunityCanvasTextFonts), FontConfigExtension(CommunityCanvasTextFonts),
blocks.EditorSettingExtension(mockEditorSetting()),
], ],
extensions: { extensions: {
FontConfigExtension: FontConfigExtension(CommunityCanvasTextFonts), FontConfigExtension: FontConfigExtension(CommunityCanvasTextFonts),
@@ -7,6 +7,7 @@ import {
AffineFormatBarWidget, AffineFormatBarWidget,
CommunityCanvasTextFonts, CommunityCanvasTextFonts,
DocModeProvider, DocModeProvider,
EditorSettingExtension,
FontConfigExtension, FontConfigExtension,
GenerateDocUrlExtension, GenerateDocUrlExtension,
NotificationExtension, NotificationExtension,
@@ -35,6 +36,7 @@ import {
} from '../../_common/history.js'; } from '../../_common/history.js';
import { import {
mockDocModeService, mockDocModeService,
mockEditorSetting,
mockGenerateDocUrlService, mockGenerateDocUrlService,
mockNotificationService, mockNotificationService,
mockParseDocUrlService, mockParseDocUrlService,
@@ -79,6 +81,7 @@ export async function mountDefaultDocEditor(collection: DocCollection) {
GenerateDocUrlExtension(mockGenerateDocUrlService(collection)), GenerateDocUrlExtension(mockGenerateDocUrlService(collection)),
NotificationExtension(mockNotificationService(editor)), NotificationExtension(mockNotificationService(editor)),
OverrideThemeExtension(themeExtension), OverrideThemeExtension(themeExtension),
EditorSettingExtension(mockEditorSetting()),
{ {
setup: di => { setup: di => {
di.override(DocModeProvider, () => di.override(DocModeProvider, () =>
@@ -120,6 +120,50 @@ test('zoom by mouse', async ({ page }) => {
await assertEdgelessSelectedModelRect(page, zoomed); await assertEdgelessSelectedModelRect(page, zoomed);
}); });
test('zoom by mouse without ctrl pressed when edgelessScrollZoom is enabled', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertZoomLevel(page, 100);
await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]);
await page.mouse.click(CENTER_X, CENTER_Y);
const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT];
await assertEdgelessSelectedModelRect(page, original);
// enable edgelessScrollZoom
await page.evaluate(() => {
window.editorSetting$.value = {
...window.editorSetting$.value,
edgelessScrollZoom: true,
};
});
// can zoom without ctrl pressed
await zoomByMouseWheel(page, 0, 125, false);
await assertZoomLevel(page, 90);
const zoomed = [0, 0, original[2] * 0.9, original[3] * 0.9];
await assertEdgelessSelectedModelRect(page, zoomed);
// disable edgelessScrollZoom
await page.evaluate(() => {
window.editorSetting$.value = {
...window.editorSetting$.value,
edgelessScrollZoom: false,
};
});
// can't zoom without ctrl pressed
await zoomByMouseWheel(page, 0, 125, false);
await assertZoomLevel(page, 90);
});
test('zoom by pinch', async ({ page }) => { test('zoom by pinch', async ({ page }) => {
await enterPlaygroundRoom(page); await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page); await initEmptyEdgelessState(page);
@@ -825,11 +825,14 @@ export async function clickComponentToolbarMoreMenuButton(
export async function zoomByMouseWheel( export async function zoomByMouseWheel(
page: Page, page: Page,
stepX: number, stepX: number,
stepY: number stepY: number,
pressedKey = true
) { ) {
await page.keyboard.down(SHORT_KEY); if (pressedKey) await page.keyboard.down(SHORT_KEY);
await page.mouse.wheel(stepX, stepY); await page.mouse.wheel(stepX, stepY);
await page.keyboard.up(SHORT_KEY);
if (pressedKey) await page.keyboard.up(SHORT_KEY);
} }
// touch screen is not supported by Playwright now // touch screen is not supported by Playwright now
@@ -1,10 +1,10 @@
import { Menu, MenuItem, MenuTrigger } from '@affine/component'; import { Menu, MenuItem, MenuTrigger, Switch } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-setting'; import { EditorSettingService } from '@affine/core/modules/editor-setting';
import type { EdgelessDefaultTheme } from '@affine/core/modules/editor-setting/schema'; import type { EdgelessDefaultTheme } from '@affine/core/modules/editor-setting/schema';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { menuTrigger } from '../style.css'; import { menuTrigger } from '../style.css';
@@ -78,29 +78,51 @@ export const GeneralEdgelessSetting = () => {
}); });
}, [editorSetting, items, edgelessDefaultTheme]); }, [editorSetting, items, edgelessDefaultTheme]);
const handleScrollZoomChange = useCallback(
(checked: boolean) => {
editorSetting.set('edgelessScrollZoom', checked);
},
[editorSetting]
);
return ( return (
<SettingRow <>
name={t[ <SettingRow
'com.affine.settings.editorSettings.page.edgeless-default-theme.title' name={t[
]()} 'com.affine.settings.editorSettings.page.edgeless-default-theme.title'
desc={t[ ]()}
'com.affine.settings.editorSettings.page.edgeless-default-theme.description' desc={t[
]()} 'com.affine.settings.editorSettings.page.edgeless-default-theme.description'
> ]()}
<Menu
items={menuItems}
contentOptions={{
align: 'end',
sideOffset: 16,
style: {
width: '280px',
},
}}
> >
<MenuTrigger tooltip={currentTheme} className={menuTrigger}> <Menu
{currentTheme} items={menuItems}
</MenuTrigger> contentOptions={{
</Menu> align: 'end',
</SettingRow> sideOffset: 16,
style: {
width: '280px',
},
}}
>
<MenuTrigger tooltip={currentTheme} className={menuTrigger}>
{currentTheme}
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.title'
]()}
desc={t[
'com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.description'
]()}
>
<Switch
checked={editorSetting.edgelessScrollZoom.$.value}
onChange={handleScrollZoomChange}
></Switch>
</SettingRow>
</>
); );
}; };
@@ -1,7 +1,7 @@
import { NodePropsSchema } from '@blocksuite/affine-shared/utils'; import { GeneralSettingSchema } from '@blocksuite/affine-shared/services';
import { z } from 'zod'; import { z } from 'zod';
export const BSEditorSettingSchema = NodePropsSchema; export const BSEditorSettingSchema = GeneralSettingSchema;
export type FontFamily = 'Sans' | 'Serif' | 'Mono' | 'Custom'; export type FontFamily = 'Sans' | 'Serif' | 'Mono' | 'Custom';
export type EdgelessDefaultTheme = 'auto' | 'dark' | 'light' | 'specified'; export type EdgelessDefaultTheme = 'auto' | 'dark' | 'light' | 'specified';
@@ -32,5 +32,5 @@ export const EditorSettingSchema = BSEditorSettingSchema.merge(
AffineEditorSettingSchema AffineEditorSettingSchema
); );
// eslint-disable-next-line no-redeclare // oxlint-disable-next-line no-redeclare
export type EditorSettingSchema = z.infer<typeof EditorSettingSchema>; export type EditorSettingSchema = z.infer<typeof EditorSettingSchema>;
@@ -1236,6 +1236,8 @@
"com.affine.settings.editorSettings.page.edgeless-default-theme.description": "Set edgeless default color scheme.", "com.affine.settings.editorSettings.page.edgeless-default-theme.description": "Set edgeless default color scheme.",
"com.affine.settings.editorSettings.page.edgeless-default-theme.title": "Edgeless default theme", "com.affine.settings.editorSettings.page.edgeless-default-theme.title": "Edgeless default theme",
"com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "Specified by current color mode", "com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "Specified by current color mode",
"com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.title": "Scroll wheel zoom",
"com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.description": "Use the scroll wheel to zoom in and out.",
"com.affine.settings.editorSettings.preferences": "Preferences", "com.affine.settings.editorSettings.preferences": "Preferences",
"com.affine.settings.editorSettings.preferences.export.description": "You can export the entire preferences data for backup, and the exported data can be re-imported.", "com.affine.settings.editorSettings.preferences.export.description": "You can export the entire preferences data for backup, and the exported data can be re-imported.",
"com.affine.settings.editorSettings.preferences.export.title": "Export Settings", "com.affine.settings.editorSettings.preferences.export.title": "Export Settings",
@@ -1143,6 +1143,8 @@
"com.affine.settings.editorSettings.page.edgeless-default-theme.description": "设置默认的无界配色方案。", "com.affine.settings.editorSettings.page.edgeless-default-theme.description": "设置默认的无界配色方案。",
"com.affine.settings.editorSettings.page.edgeless-default-theme.title": "无界默认配色方案", "com.affine.settings.editorSettings.page.edgeless-default-theme.title": "无界默认配色方案",
"com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "由当前应用的配色方案指定", "com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "由当前应用的配色方案指定",
"com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.title": "滚轮缩放",
"com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.description": "使用鼠标滚轮来调整缩放比例",
"com.affine.settings.editorSettings.preferences": "首选项", "com.affine.settings.editorSettings.preferences": "首选项",
"com.affine.settings.editorSettings.preferences.export.description": "您可以导出整个首选项数据进行备份,然后可以重新导入导出的数据。", "com.affine.settings.editorSettings.preferences.export.description": "您可以导出整个首选项数据进行备份,然后可以重新导入导出的数据。",
"com.affine.settings.editorSettings.preferences.export.title": "导出设置", "com.affine.settings.editorSettings.preferences.export.title": "导出设置",
@@ -1140,6 +1140,8 @@
"com.affine.settings.editorSettings.page.edgeless-default-theme.description": "設定無界的預設配色。", "com.affine.settings.editorSettings.page.edgeless-default-theme.description": "設定無界的預設配色。",
"com.affine.settings.editorSettings.page.edgeless-default-theme.title": "無界預設主題", "com.affine.settings.editorSettings.page.edgeless-default-theme.title": "無界預設主題",
"com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "由目前顏色模式指定", "com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "由目前顏色模式指定",
"com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.title": "滾輪縮放",
"com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.description": "使用滑鼠滾輪來調整縮放比例",
"com.affine.settings.editorSettings.preferences": "首選項", "com.affine.settings.editorSettings.preferences": "首選項",
"com.affine.settings.editorSettings.preferences.export.description": "您可以匯出整個首選項數據進行備份,導出的數據亦可重新匯入。", "com.affine.settings.editorSettings.preferences.export.description": "您可以匯出整個首選項數據進行備份,導出的數據亦可重新匯入。",
"com.affine.settings.editorSettings.preferences.export.title": "匯出設定", "com.affine.settings.editorSettings.preferences.export.title": "匯出設定",