refactor(editor): remove gfx tool global type (#12116)

Closes: BS-2650
This commit is contained in:
Saul-Mirone
2025-05-04 13:53:26 +00:00
parent f3b5c36cf7
commit 30a2e5b4fb
95 changed files with 664 additions and 521 deletions

View File

@@ -1,3 +1,4 @@
import { DefaultTool } from '@blocksuite/affine-block-surface';
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import {
@@ -5,6 +6,7 @@ import {
SlashMenuConfigIdentifier,
} from '@blocksuite/affine-widget-slash-menu';
import { LinkIcon } from '@blocksuite/icons/lit';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { ExtensionType } from '@blocksuite/store';
import { LinkTooltip } from './tooltips';
@@ -33,7 +35,13 @@ const bookmarkSlashMenuConfig: SlashMenuConfig = {
host,
'Links',
'The added link will be displayed as a card view.',
{ mode: 'page', parentModel, index }
{ mode: 'page', parentModel, index },
({ mode }) => {
if (mode === 'edgeless') {
const gfx = std.get(GfxControllerIdentifier);
gfx.tool.setTool(DefaultTool);
}
}
)
.then(() => {
if (model.text?.length === 0) {

View File

@@ -13,6 +13,7 @@
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-gfx-pointer": "workspace:*",
"@blocksuite/affine-inline-reference": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",

View File

@@ -1,4 +1,5 @@
import {
DefaultTool,
EdgelessCRUDIdentifier,
SurfaceBlockComponent,
} from '@blocksuite/affine-block-surface';
@@ -82,10 +83,7 @@ export function insertEmbedCard(
surfaceBlock.model
);
gfx.tool.setTool(
// @ts-expect-error FIXME: resolve after gfx tool refactor
'default'
);
gfx.tool.setTool(DefaultTool);
gfx.selection.set({
elements: [cardId],
editing: false,

View File

@@ -1,6 +1,8 @@
import { DefaultTool } from '@blocksuite/affine-block-surface';
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import type { SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { FigmaDuotoneIcon } from '@blocksuite/icons/lit';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { FigmaTooltip } from './tooltips';
@@ -29,7 +31,13 @@ export const embedFigmaSlashMenuConfig: SlashMenuConfig = {
host,
'Figma',
'The added Figma link will be displayed as an embed view.',
{ mode: 'page', parentModel, index }
{ mode: 'page', parentModel, index },
({ mode }) => {
if (mode === 'edgeless') {
const gfx = std.get(GfxControllerIdentifier);
gfx.tool.setTool(DefaultTool);
}
}
);
if (model.text?.length === 0) std.store.deleteBlock(model);
})().catch(console.error);

View File

@@ -1,6 +1,8 @@
import { DefaultTool } from '@blocksuite/affine-block-surface';
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import type { SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { GithubDuotoneIcon } from '@blocksuite/icons/lit';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { GithubRepoTooltip } from './tooltips';
@@ -29,7 +31,13 @@ export const embedGithubSlashMenuConfig: SlashMenuConfig = {
host,
'GitHub',
'The added GitHub issue or pull request link will be displayed as a card view.',
{ mode: 'page', parentModel, index }
{ mode: 'page', parentModel, index },
({ mode }) => {
if (mode === 'edgeless') {
const gfx = std.get(GfxControllerIdentifier);
gfx.tool.setTool(DefaultTool);
}
}
);
if (model.text?.length === 0) std.store.deleteBlock(model);
})().catch(console.error);

View File

@@ -1,4 +1,5 @@
import {
DefaultTool,
EdgelessCRUDIdentifier,
SurfaceBlockComponent,
} from '@blocksuite/affine-block-surface';
@@ -90,10 +91,7 @@ export const insertEmbedIframeWithUrlCommand: Command<
surfaceBlock.model
);
gfx.tool.setTool(
// @ts-expect-error FIXME: resolve after gfx tool refactor
'default'
);
gfx.tool.setTool(DefaultTool);
gfx.selection.set({
elements: [newBlockId],

View File

@@ -1,6 +1,8 @@
import { DefaultTool } from '@blocksuite/affine-block-surface';
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import type { SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { LoomLogoDuotoneIcon } from '@blocksuite/icons/lit';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { LoomTooltip } from './tooltips';
@@ -29,7 +31,13 @@ export const embedLoomSlashMenuConfig: SlashMenuConfig = {
host,
'Loom',
'The added Loom video link will be displayed as an embed view.',
{ mode: 'page', parentModel, index }
{ mode: 'page', parentModel, index },
({ mode }) => {
if (mode === 'edgeless') {
const gfx = std.get(GfxControllerIdentifier);
gfx.tool.setTool(DefaultTool);
}
}
);
if (model.text?.length === 0) std.store.deleteBlock(model);
})().catch(console.error);

View File

@@ -1,6 +1,8 @@
import { DefaultTool } from '@blocksuite/affine-block-surface';
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import type { SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { YoutubeDuotoneIcon } from '@blocksuite/icons/lit';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { YoutubeVideoTooltip } from './tooltips';
@@ -29,7 +31,13 @@ export const embedYoutubeSlashMenuConfig: SlashMenuConfig = {
host,
'YouTube',
'The added YouTube video link will be displayed as an embed view.',
{ mode: 'page', parentModel, index }
{ mode: 'page', parentModel, index },
({ mode }) => {
if (mode === 'edgeless') {
const gfx = std.get(GfxControllerIdentifier);
gfx.tool.setTool(DefaultTool);
}
}
);
if (model.text?.length === 0) std.store.deleteBlock(model);
})().catch(console.error);

View File

@@ -10,6 +10,7 @@
{ "path": "../surface" },
{ "path": "../../components" },
{ "path": "../../ext-loader" },
{ "path": "../../gfx/pointer" },
{ "path": "../../inlines/reference" },
{ "path": "../../model" },
{ "path": "../../rich-text" },

View File

@@ -13,6 +13,7 @@
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-gfx-pointer": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",

View File

@@ -1,29 +1,30 @@
import { DefaultTool } from '@blocksuite/affine-block-surface';
import { menu } from '@blocksuite/affine-components/context-menu';
import type { DenseMenuBuilder } from '@blocksuite/affine-widget-edgeless-toolbar';
import { FrameIcon } from '@blocksuite/icons/lit';
import { EdgelessFrameManagerIdentifier } from '../frame-manager.js';
import { FrameTool } from '../frame-tool';
import { FrameConfig } from './config.js';
export const buildFrameDenseMenu: DenseMenuBuilder = (edgeless, gfx) =>
menu.subMenu({
name: 'Frame',
prefix: FrameIcon({ width: '20px', height: '20px' }),
select: () => gfx.tool.setTool({ type: 'frame' }),
select: () => gfx.tool.setTool(FrameTool),
isSelected: gfx.tool.currentToolName$.peek() === 'frame',
options: {
items: [
menu.action({
name: 'Custom',
select: () => gfx.tool.setTool({ type: 'frame' }),
select: () => gfx.tool.setTool(FrameTool),
}),
...FrameConfig.map(config =>
menu.action({
name: `Slide ${config.name}`,
select: () => {
const frame = edgeless.std.get(EdgelessFrameManagerIdentifier);
// @ts-expect-error FIXME: resolve after gfx tool refactor
gfx.tool.setTool('default');
gfx.tool.setTool(DefaultTool);
frame.createFrameOnViewportCenter(config.wh);
},
})

View File

@@ -1,9 +1,10 @@
import { DefaultTool } from '@blocksuite/affine-block-surface';
import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
import type { GfxToolsFullOptionValue } from '@blocksuite/std/gfx';
import { css, html, LitElement } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { EdgelessFrameManagerIdentifier } from '../frame-manager.js';
import { FrameTool } from '../frame-tool';
import { FrameConfig } from './config.js';
export class EdgelessFrameMenu extends EdgelessToolbarToolMixin(LitElement) {
@@ -65,7 +66,7 @@ export class EdgelessFrameMenu extends EdgelessToolbarToolMixin(LitElement) {
}
`;
override type: GfxToolsFullOptionValue['type'] = 'frame';
override type = FrameTool;
get frameManager() {
return this.edgeless.std.get(EdgelessFrameManagerIdentifier);
@@ -84,8 +85,7 @@ export class EdgelessFrameMenu extends EdgelessToolbarToolMixin(LitElement) {
(item, index) => html`
<div
@click=${() => {
// @ts-expect-error FIXME: resolve after gfx tool refactor
gfx.tool.setTool('default');
gfx.tool.setTool(DefaultTool);
frameManager.createFrameOnViewportCenter(item.wh);
}}
class="frame-add-button ${index}"

View File

@@ -1,8 +1,9 @@
import { QuickToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
import { FrameIcon } from '@blocksuite/icons/lit';
import type { GfxToolsFullOptionValue } from '@blocksuite/std/gfx';
import { css, html, LitElement } from 'lit';
import { FrameTool } from '../frame-tool';
export class EdgelessFrameToolButton extends QuickToolMixin(LitElement) {
static override styles = css`
:host {
@@ -10,7 +11,7 @@ export class EdgelessFrameToolButton extends QuickToolMixin(LitElement) {
}
`;
override type: GfxToolsFullOptionValue['type'] = 'frame';
override type = FrameTool;
private _toggleFrameMenu() {
if (this.tryDisposePopper()) return;
@@ -20,7 +21,7 @@ export class EdgelessFrameToolButton extends QuickToolMixin(LitElement) {
}
override render() {
const type = this.edgelessTool?.type;
const type = this.edgelessTool?.toolType?.toolName;
return html`
<edgeless-tool-icon-button
class="edgeless-frame-button"
@@ -37,7 +38,7 @@ export class EdgelessFrameToolButton extends QuickToolMixin(LitElement) {
@click=${() => {
// don't update tool before toggling menu
this._toggleFrameMenu();
this.setEdgelessTool({ type: 'frame' });
this.setEdgelessTool(FrameTool);
}}
>
${FrameIcon()}

View File

@@ -1,5 +1,9 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import {
DefaultTool,
EdgelessLegacySlotIdentifier,
} from '@blocksuite/affine-block-surface';
import { toast } from '@blocksuite/affine-components/toast';
import { PanTool } from '@blocksuite/affine-gfx-pointer';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import {
EditPropsStore,
@@ -16,7 +20,7 @@ import {
StopAiIcon,
} from '@blocksuite/icons/lit';
import type { BlockComponent } from '@blocksuite/std';
import type { GfxToolsFullOptionValue } from '@blocksuite/std/gfx';
import type { ToolOptions } from '@blocksuite/std/gfx';
import { effect } from '@preact/signals-core';
import { cssVar } from '@toeverything/theme';
import { css, html, LitElement, nothing, type PropertyValues } from 'lit';
@@ -27,6 +31,7 @@ import {
isFrameBlock,
type NavigatorMode,
} from '../frame-manager';
import { PresentTool } from '../present-tool';
export class PresentationToolbar extends EdgelessToolbarToolMixin(
SignalWatcher(LitElement)
@@ -114,7 +119,7 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
private _timer?: ReturnType<typeof setTimeout>;
override type: GfxToolsFullOptionValue['type'] = 'frameNavigator';
override type = PresentTool;
private get _cachedPresentHideToolbar() {
return !!this.edgeless.std
@@ -151,7 +156,7 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
private _bindHotKey() {
const handleKeyIfFrameNavigator = (action: () => void) => () => {
if (this.edgelessTool.type === 'frameNavigator') {
if (this.edgelessTool.toolType === PresentTool) {
action();
}
};
@@ -171,11 +176,11 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
private _exitPresentation() {
// When exit presentation mode, we need to set the tool to default or pan
// And exit fullscreen
const tool = this.edgeless.doc.readonly
? { type: 'pan', panning: false }
: { type: 'default' };
// @ts-expect-error FIXME: resolve after gfx tool refactor
this.setEdgelessTool(tool);
if (this.edgeless.doc.readonly) {
this.setEdgelessTool(PanTool, { panning: false });
} else {
this.setEdgelessTool(DefaultTool);
}
if (document.fullscreenElement) {
document.exitFullscreen().catch(console.error);
@@ -261,9 +266,11 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
const currentTool = this.gfx.tool.currentToolOption$.value;
const selection = this.gfx.selection;
if (currentTool?.type === 'frameNavigator') {
if (currentTool?.toolType === PresentTool) {
this._cachedIndex = this._currentFrameIndex;
this._navigatorMode = currentTool.mode ?? this._navigatorMode;
this._navigatorMode =
(currentTool.options as ToolOptions<PresentTool>)?.mode ??
this._navigatorMode;
if (isFrameBlock(selection.selectedElements[0])) {
this._cachedIndex = this._frames.findIndex(
frame => frame.id === selection.selectedElements[0].id
@@ -306,14 +313,14 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
// When exit fullscreen, we need to clear the timer
clearTimeout(this._timer);
if (
this.edgelessTool.type === 'frameNavigator' &&
this.edgelessTool.toolType === PresentTool &&
this._fullScreenMode
) {
const tool = this.edgeless.doc.readonly
? { type: 'pan', panning: false }
: { type: 'default' };
// @ts-expect-error FIXME: resolve after gfx tool refactor
this.setEdgelessTool(tool);
if (this.edgeless.doc.readonly) {
this.setEdgelessTool(PanTool, { panning: false });
} else {
this.setEdgelessTool(DefaultTool);
}
}
}
@@ -425,7 +432,7 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
protected override updated(changedProperties: PropertyValues) {
if (
changedProperties.has('_currentFrameIndex') &&
this.edgelessTool.type === 'frameNavigator'
this.edgelessTool.toolType === PresentTool
) {
this._moveToCurrentFrame();
}

View File

@@ -1,4 +1,7 @@
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
import {
DefaultTool,
OverlayIdentifier,
} from '@blocksuite/affine-block-surface';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import {
EditPropsStore,
@@ -39,8 +42,7 @@ export class FrameTool extends BaseTool {
if (this._frame) {
const frame = this._frame;
frame.pop('xywh');
// @ts-expect-error TODO: refactor gfx tool
this.gfx.tool.setTool('default');
this.gfx.tool.setTool(DefaultTool);
this.gfx.selection.set({
elements: [frame.id],
editing: false,

View File

@@ -1,5 +1,5 @@
import type { FrameTool } from './frame-tool';
import type { PresentTool, PresentToolOption } from './preset-tool';
import type { PresentTool, PresentToolOption } from './present-tool';
export * from './edgeless-clipboard-config';
export * from './edgeless-toolbar';
@@ -9,7 +9,7 @@ export * from './frame-manager';
export * from './frame-spec';
export * from './frame-tool';
export * from './frame-toolbar';
export * from './preset-tool';
export * from './present-tool';
declare module '@blocksuite/std/gfx' {
interface GfxToolsMap {

View File

@@ -9,6 +9,8 @@ import { css, html, nothing } from 'lit';
import { state } from 'lit/decorators.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { PresentTool } from '../present-tool';
export const EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET =
'edgeless-navigator-black-background';
export class EdgelessNavigatorBlackBackgroundWidget extends WidgetComponent<RootBlockModel> {
@@ -59,7 +61,7 @@ export class EdgelessNavigatorBlackBackgroundWidget extends WidgetComponent<Root
this.show =
blackBackground &&
this.gfx.tool.currentToolOption$.peek().type === 'frameNavigator';
this.gfx.tool.currentToolOption$.peek().toolType === PresentTool;
}
})
);

View File

@@ -3,9 +3,10 @@ import {
QuickToolMixin,
} from '@blocksuite/affine-widget-edgeless-toolbar';
import { PresentationIcon } from '@blocksuite/icons/lit';
import type { GfxToolsFullOptionValue } from '@blocksuite/std/gfx';
import { css, html, LitElement } from 'lit';
import { PresentTool } from '../present-tool';
export class EdgelessPresentButton extends QuickToolMixin(
EdgelessToolbarToolMixin(LitElement)
) {
@@ -19,7 +20,7 @@ export class EdgelessPresentButton extends QuickToolMixin(
}
`;
override type: GfxToolsFullOptionValue['type'] = 'frameNavigator';
override type = PresentTool;
override render() {
return html`<edgeless-tool-icon-button
@@ -29,9 +30,7 @@ export class EdgelessPresentButton extends QuickToolMixin(
.iconContainerPadding=${6}
.iconSize=${'24px'}
@click=${() => {
this.setEdgelessTool({
type: 'frameNavigator',
});
this.setEdgelessTool(PresentTool);
}}
>
${PresentationIcon()}

View File

@@ -11,7 +11,7 @@ import { FrameBlockSpec } from './frame-spec';
import { FrameTool } from './frame-tool';
import { frameToolbarExtension } from './frame-toolbar';
import { edgelessNavigatorBgWidget } from './present/navigator-bg-widget';
import { PresentTool } from './preset-tool';
import { PresentTool } from './present-tool';
export class FrameViewExtension extends ViewExtensionProvider {
override name = 'affine-frame-block';

View File

@@ -10,6 +10,7 @@
{ "path": "../surface" },
{ "path": "../../components" },
{ "path": "../../ext-loader" },
{ "path": "../../gfx/pointer" },
{ "path": "../../model" },
{ "path": "../../shared" },
{ "path": "../../widgets/edgeless-toolbar" },

View File

@@ -2,6 +2,7 @@ import { addAttachments } from '@blocksuite/affine-block-attachment';
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
import { addImages } from '@blocksuite/affine-block-image';
import {
DefaultTool,
EdgelessCRUDIdentifier,
ExportManager,
getSurfaceComponent,
@@ -608,7 +609,7 @@ export class EdgelessClipboardController extends PageClipboard {
elements: [noteId],
editing: false,
});
this.gfx.tool.setTool('default');
this.gfx.tool.setTool(DefaultTool);
}
copy() {

View File

@@ -1,7 +1,7 @@
import {
DefaultModeDragType,
DefaultTool,
} from '@blocksuite/affine-gfx-pointer';
} from '@blocksuite/affine-block-surface';
import type { RootBlockModel } from '@blocksuite/affine-model';
import { WidgetComponent } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
@@ -41,6 +41,7 @@ export class EdgelessDraggingAreaRectWidget extends WidgetComponent<RootBlockMod
if (
rect.w === 0 ||
rect.h === 0 ||
!tool ||
!(tool instanceof DefaultTool) ||
tool.dragType !== DefaultModeDragType.Selecting
)

View File

@@ -3,6 +3,7 @@ import {
EdgelessCRUDIdentifier,
getSurfaceComponent,
} from '@blocksuite/affine-block-surface';
import { ConnectorTool } from '@blocksuite/affine-gfx-connector';
import {
createGroupCommand,
createGroupFromSelectedCommand,
@@ -196,9 +197,9 @@ export const builtinMiscToolbarConfig = {
const point = ctx.gfx.viewport.toViewCoordFromClientCoord([x, y]);
ctx.store.captureSync();
ctx.gfx.tool.setTool('connector', { mode: DEFAULT_CONNECTOR_MODE });
ctx.gfx.tool.setTool(ConnectorTool, { mode: DEFAULT_CONNECTOR_MODE });
const ctc = ctx.gfx.tool.get('connector');
const ctc = ctx.gfx.tool.get(ConnectorTool);
ctc.quickConnect(point, models[0]);
};

View File

@@ -1,8 +1,17 @@
import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmark';
import { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text';
import { isNoteBlock } from '@blocksuite/affine-block-surface';
import { FrameTool } from '@blocksuite/affine-block-frame';
import { DefaultTool, isNoteBlock } from '@blocksuite/affine-block-surface';
import { toast } from '@blocksuite/affine-components/toast';
import { mountConnectorLabelEditor } from '@blocksuite/affine-gfx-connector';
import {
BrushTool,
EraserTool,
HighlighterTool,
} from '@blocksuite/affine-gfx-brush';
import {
ConnectorTool,
mountConnectorLabelEditor,
} from '@blocksuite/affine-gfx-connector';
import {
createGroupFromSelectedCommand,
ungroupCommand,
@@ -12,7 +21,10 @@ import {
isElementOutsideViewport,
isSingleMindMapNode,
} from '@blocksuite/affine-gfx-mindmap';
import { NoteTool } from '@blocksuite/affine-gfx-note';
import { PanTool } from '@blocksuite/affine-gfx-pointer';
import { mountShapeTextEditor, ShapeTool } from '@blocksuite/affine-gfx-shape';
import { TextTool } from '@blocksuite/affine-gfx-text';
import {
ConnectorElementModel,
ConnectorMode,
@@ -33,11 +45,12 @@ import { IS_MAC } from '@blocksuite/global/env';
import { Bound, getCommonBound } from '@blocksuite/global/gfx';
import { SurfaceSelection, TextSelection } from '@blocksuite/std';
import {
type BaseTool,
GfxBlockElementModel,
type GfxPrimitiveElementModel,
type GfxToolsMap,
type GfxToolsOption,
isGfxGroupCompatibleModel,
type ToolOptions,
type ToolType,
} from '@blocksuite/std/gfx';
import { PageKeyboardManager } from '../keyboard/keyboard-manager.js';
@@ -61,38 +74,38 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
this.rootComponent.bindHotKey(
{
v: () => {
this._setEdgelessTool('default');
this._setEdgelessTool(DefaultTool);
},
t: () => {
this._setEdgelessTool('text');
this._setEdgelessTool(TextTool);
},
c: () => {
const mode = ConnectorMode.Curve;
rootComponent.std.get(EditPropsStore).recordLastProps('connector', {
mode,
});
this._setEdgelessTool('connector', { mode });
this._setEdgelessTool(ConnectorTool, { mode });
},
h: () => {
this._setEdgelessTool('pan', {
this._setEdgelessTool(PanTool, {
panning: false,
});
},
n: () => {
this._setEdgelessTool('affine:note', {
this._setEdgelessTool(NoteTool, {
childFlavour: DEFAULT_NOTE_CHILD_FLAVOUR,
childType: DEFAULT_NOTE_CHILD_TYPE,
tip: DEFAULT_NOTE_TIP,
});
},
p: () => {
this._setEdgelessTool('brush');
this._setEdgelessTool(BrushTool);
},
'Shift-p': () => {
this._setEdgelessTool('highlighter');
this._setEdgelessTool(HighlighterTool);
},
e: () => {
this._setEdgelessTool('eraser');
this._setEdgelessTool(EraserTool);
},
k: () => {
if (this.rootComponent.service.locked) return;
@@ -128,7 +141,7 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
});
rootComponent.surface.fitToViewport(Bound.deserialize(frame.xywh));
} else if (!this.rootComponent.service.selection.editing) {
this._setEdgelessTool('frame');
this._setEdgelessTool(FrameTool);
}
},
'-': () => {
@@ -184,7 +197,7 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
}
const { shapeName } = controller.activatedOption;
const nextShapeName = getNextShapeType(shapeName);
this._setEdgelessTool('shape', {
this._setEdgelessTool(ShapeTool, {
shapeName: nextShapeName,
});
@@ -633,11 +646,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
});
}
private _setEdgelessTool<K extends keyof GfxToolsMap>(
toolName: K,
...options: K extends keyof GfxToolsOption
? [option: GfxToolsOption[K], ignoreActiveState?: boolean]
: [option: void, ignoreActiveState?: boolean]
private _setEdgelessTool<T extends BaseTool>(
toolType: ToolType<T>,
...options: [options?: ToolOptions<T>, ignoreActiveState?: boolean]
) {
const ignoreActiveState =
typeof options === 'boolean'
@@ -651,9 +662,8 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
return;
}
this.rootComponent.gfx.tool.setTool<K>(
toolName,
// @ts-expect-error FIXME: ts error
this.rootComponent.gfx.tool.setTool(
toolType,
options[0] !== undefined && typeof options[0] !== 'boolean'
? options[0]
: undefined
@@ -678,8 +688,7 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
const revertToPrevTool = (ev: KeyboardEvent) => {
if (ev.code === 'Space') {
this._setEdgelessTool(
// @ts-expect-error FIXME: ts error
currentTool.toolName,
(currentTool as DefaultTool).constructor as typeof DefaultTool,
currentTool?.activatedOption
);
selection.set(currentSel);
@@ -694,7 +703,7 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
) {
return;
}
this._setEdgelessTool('pan', { panning: false });
this._setEdgelessTool(PanTool, { panning: false });
edgeless.dispatcher.disposables.addFromEvent(
document,

View File

@@ -1,14 +1,14 @@
import { NoteConfigExtension } from '@blocksuite/affine-block-note';
import type {
SurfaceBlockComponent,
SurfaceBlockModel,
} from '@blocksuite/affine-block-surface';
import {
DefaultTool,
EdgelessLegacySlotIdentifier,
getBgGridGap,
normalizeWheelDeltaY,
type SurfaceBlockComponent,
type SurfaceBlockModel,
} from '@blocksuite/affine-block-surface';
import { isSingleMindMapNode } from '@blocksuite/affine-gfx-mindmap';
import { PanTool } from '@blocksuite/affine-gfx-pointer';
import { mountShapeTextEditor } from '@blocksuite/affine-gfx-shape';
import {
NoteBlockModel,
@@ -468,9 +468,9 @@ export class EdgelessRootBlockComponent extends BlockComponent<
this._initPinchEvent();
if (this.doc.readonly) {
this.gfx.tool.setTool('pan', { panning: true });
this.gfx.tool.setTool(PanTool, { panning: true });
} else {
this.gfx.tool.setTool('default');
this.gfx.tool.setTool(DefaultTool);
}
this.gfx.viewport.elementReady.next(this.gfxViewportElm);

View File

@@ -1,4 +1,5 @@
import type { CanvasElementWithText } from '@blocksuite/affine-block-surface';
import type { PanTool } from '@blocksuite/affine-gfx-pointer';
import {
type AttachmentBlockModel,
type BookmarkBlockModel,
@@ -26,7 +27,7 @@ import { Bound } from '@blocksuite/global/gfx';
import type {
GfxModel,
GfxPrimitiveElementModel,
GfxToolsFullOptionValue,
ToolOptionWithType,
} from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
@@ -191,15 +192,17 @@ export function isConnectable(
}
// https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
export function getCursorMode(edgelessTool: GfxToolsFullOptionValue | null) {
export function getCursorMode(edgelessTool: ToolOptionWithType) {
if (!edgelessTool) {
return 'default';
}
switch (edgelessTool.type) {
switch (edgelessTool.toolType?.toolName) {
case 'default':
return 'default';
case 'pan':
return edgelessTool.panning ? 'grabbing' : 'grab';
return (edgelessTool as ToolOptionWithType<PanTool>).options?.panning
? 'grabbing'
: 'grab';
case 'brush':
case 'highlighter':
return drawingCursor;

View File

@@ -32,9 +32,8 @@ export {
PageSurfaceBlockSpec,
} from './surface-spec.js';
export { SurfaceBlockTransformer } from './surface-transformer.js';
export * from './tool/default-tool.js';
export {
addNote,
addNoteAtPoint,
generateElementId,
getBgGridGap,
getLastPropsKey,

View File

@@ -0,0 +1,424 @@
import { resetNativeSelection } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { IVec } from '@blocksuite/global/gfx';
import type { PointerEventState } from '@blocksuite/std';
import {
BaseTool,
type GfxModel,
InteractivityIdentifier,
isGfxGroupCompatibleModel,
} from '@blocksuite/std/gfx';
import { effect } from '@preact/signals-core';
import { calPanDelta } from './panning-utils.js';
export enum DefaultModeDragType {
/** Moving selected contents */
ContentMoving = 'content-moving',
/** Native range dragging inside active note block */
NativeEditing = 'native-editing',
/** Default void state */
None = 'none',
/** Expanding the dragging area, select the content covered inside */
Selecting = 'selecting',
}
export class DefaultTool extends BaseTool {
static override toolName: string = 'default';
private _accumulateDelta: IVec = [0, 0];
private _autoPanTimer: number | null = null;
private readonly _clearDisposable = () => {
if (this._disposables) {
this._disposables.dispose();
this._disposables = null;
}
};
private readonly _clearSelectingState = () => {
this._stopAutoPanning();
this._clearDisposable();
};
private _disposables: DisposableGroup | null = null;
private _panViewport(delta: IVec) {
this._accumulateDelta[0] += delta[0];
this._accumulateDelta[1] += delta[1];
this.gfx.viewport.applyDeltaCenter(delta[0], delta[1]);
}
private _selectionRectTransition: null | {
w: number;
h: number;
startX: number;
startY: number;
endX: number;
endY: number;
} = null;
private readonly _startAutoPanning = (delta: IVec) => {
this._panViewport(delta);
this._updateSelectingState(delta);
this._stopAutoPanning();
this._autoPanTimer = window.setInterval(() => {
this._panViewport(delta);
this._updateSelectingState(delta);
}, 30);
};
private readonly _stopAutoPanning = () => {
if (this._autoPanTimer) {
clearTimeout(this._autoPanTimer);
this._autoPanTimer = null;
}
};
private _toBeMoved: GfxModel[] = [];
private readonly _updateSelectingState = (delta: IVec = [0, 0]) => {
const { gfx } = this;
if (gfx.keyboard.spaceKey$.peek() && this._selectionRectTransition) {
/* Move the selection if space is pressed */
const curDraggingViewArea = this.controller.draggingViewArea$.peek();
const { w, h, startX, startY, endX, endY } =
this._selectionRectTransition;
const { endX: lastX, endY: lastY } = curDraggingViewArea;
const dx = lastX + delta[0] - endX + this._accumulateDelta[0];
const dy = lastY + delta[1] - endY + this._accumulateDelta[1];
this.controller.draggingViewArea$.value = {
...curDraggingViewArea,
x: Math.min(startX + dx, lastX),
y: Math.min(startY + dy, lastY),
w,
h,
startX: startX + dx,
startY: startY + dy,
};
} else {
const curDraggingArea = this.controller.draggingViewArea$.peek();
const newStartX = curDraggingArea.startX - delta[0];
const newStartY = curDraggingArea.startY - delta[1];
this.controller.draggingViewArea$.value = {
...curDraggingArea,
startX: newStartX,
startY: newStartY,
x: Math.min(newStartX, curDraggingArea.endX),
y: Math.min(newStartY, curDraggingArea.endY),
w: Math.abs(curDraggingArea.endX - newStartX),
h: Math.abs(curDraggingArea.endY - newStartY),
};
}
const elements = this.interactivity?.handleBoxSelection({
box: this.controller.draggingArea$.peek(),
});
if (!elements) return;
this.selection.set({
elements: elements.map(el => el.id),
editing: false,
});
};
dragType = DefaultModeDragType.None;
movementDragging = false;
/**
* Get the end position of the dragging area in the model coordinate
*/
get dragLastPos() {
const { endX, endY } = this.controller.draggingArea$.peek();
return [endX, endY] as IVec;
}
/**
* Get the start position of the dragging area in the model coordinate
*/
get dragStartPos() {
const { startX, startY } = this.controller.draggingArea$.peek();
return [startX, startY] as IVec;
}
get selection() {
return this.gfx.selection;
}
get interactivity() {
return this.std.getOptional(InteractivityIdentifier);
}
private async _cloneContent() {
const clonedResult = await this.interactivity?.requestElementClone({
elements: this._toBeMoved,
});
if (!clonedResult) return;
this._toBeMoved = clonedResult.elements;
this.selection.set({
elements: this._toBeMoved.map(e => e.id),
editing: false,
});
}
private _determineDragType(evt: PointerEventState): DefaultModeDragType {
const { x, y } = this.controller.lastMouseModelPos$.peek();
if (this.selection.isInSelectedRect(x, y)) {
if (this.selection.selectedElements.length === 1) {
const currentHoveredElem = this._getElementInGroup(x, y);
let curSelected = this.selection.selectedElements[0];
// If one of the following condition is true, keep the selection:
// 1. if group is currently selected
// 2. if the selected element is descendant of the hovered element
// 3. not hovering any element or hovering the same element
//
// Otherwise, we update the selection to the current hovered element
const shouldKeepSelection =
isGfxGroupCompatibleModel(curSelected) ||
(isGfxGroupCompatibleModel(currentHoveredElem) &&
currentHoveredElem.hasDescendant(curSelected)) ||
!currentHoveredElem ||
currentHoveredElem === curSelected;
if (!shouldKeepSelection) {
curSelected = currentHoveredElem;
this.selection.set({
elements: [curSelected.id],
editing: false,
});
}
}
return this.selection.editing
? DefaultModeDragType.NativeEditing
: DefaultModeDragType.ContentMoving;
} else {
const checked = this.interactivity?.handleElementSelection(evt);
if (checked) {
return DefaultModeDragType.ContentMoving;
} else {
return DefaultModeDragType.Selecting;
}
}
}
private _getElementInGroup(modelX: number, modelY: number) {
const tryGetLockedAncestor = (e: GfxModel | null) => {
if (e?.isLockedByAncestor()) {
return e.groups.findLast(group => group.isLocked());
}
return e;
};
return tryGetLockedAncestor(this.gfx.getElementInGroup(modelX, modelY));
}
private initializeDragState(
dragType: DefaultModeDragType,
event: PointerEventState
) {
this.dragType = dragType;
this._clearDisposable();
this._disposables = new DisposableGroup();
// If the drag type is selecting, set up the dragging area disposable group
// If the viewport updates when dragging, should update the dragging area and selection
if (this.dragType === DefaultModeDragType.Selecting) {
this._disposables.add(
this.gfx.viewport.viewportUpdated.subscribe(() => {
if (
this.dragType === DefaultModeDragType.Selecting &&
this.controller.dragging$.peek() &&
!this._autoPanTimer
) {
this._updateSelectingState();
}
})
);
return;
}
if (this.dragType === DefaultModeDragType.ContentMoving) {
if (this.interactivity) {
this.doc.captureSync();
this.interactivity.handleElementMove({
movingElements: this._toBeMoved,
event: event.raw,
onDragEnd: () => {
this.doc.captureSync();
},
});
}
return;
}
}
override click(e: PointerEventState) {
if (this.doc.readonly) return;
if (!this.interactivity?.handleElementSelection(e)) {
this.selection.clear();
resetNativeSelection(null);
}
this.interactivity?.dispatchEvent('click', e);
}
override deactivate() {
this._stopAutoPanning();
this._clearDisposable();
this._accumulateDelta = [0, 0];
}
override doubleClick(e: PointerEventState) {
if (this.doc.readonly) {
const viewport = this.gfx.viewport;
if (viewport.zoom === 1) {
this.gfx.fitToScreen();
} else {
// Zoom to 100% and Center
const [x, y] = viewport.toModelCoord(e.x, e.y);
viewport.setViewport(1, [x, y], true);
}
return;
}
this.interactivity?.dispatchEvent('dblclick', e);
}
override dragEnd(e: PointerEventState) {
this.interactivity?.dispatchEvent('dragend', e);
if (this.selection.editing || !this.movementDragging) return;
this.movementDragging = false;
this._toBeMoved = [];
this._clearSelectingState();
this.dragType = DefaultModeDragType.None;
}
override dragMove(e: PointerEventState) {
this.interactivity?.dispatchEvent('dragmove', e);
if (!this.movementDragging) {
return;
}
const { viewport } = this.gfx;
switch (this.dragType) {
case DefaultModeDragType.Selecting: {
// Record the last drag pointer position for auto panning and view port updating
this._updateSelectingState();
const moveDelta = calPanDelta(viewport, e);
if (moveDelta) {
this._startAutoPanning(moveDelta);
} else {
this._stopAutoPanning();
}
break;
}
case DefaultModeDragType.ContentMoving: {
break;
}
case DefaultModeDragType.NativeEditing: {
// TODO reset if drag out of note
break;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
override async dragStart(e: PointerEventState) {
const { preventDefaultState, handledByView } =
this.interactivity?.dispatchEvent('dragstart', e) ?? {};
if (this.selection.editing || preventDefaultState || handledByView) return;
this.movementDragging = true;
// Determine the drag type based on the current state and event
let dragType = this._determineDragType(e);
const elements = this.selection.selectedElements;
if (elements.some(e => e.isLocked())) return;
const toBeMoved = new Set(elements);
elements.forEach(element => {
if (isGfxGroupCompatibleModel(element)) {
element.descendantElements.forEach(ele => {
toBeMoved.add(ele);
});
}
});
this._toBeMoved = Array.from(toBeMoved);
// If alt key is pressed and content is moving, clone the content
if (dragType === DefaultModeDragType.ContentMoving && e.keys.alt) {
await this._cloneContent();
}
// Set up drag state
this.initializeDragState(dragType, e);
}
override mounted() {
this.disposable.add(
effect(() => {
const pressed = this.gfx.keyboard.spaceKey$.value;
if (pressed) {
const currentDraggingArea = this.controller.draggingViewArea$.peek();
this._selectionRectTransition = {
w: currentDraggingArea.w,
h: currentDraggingArea.h,
startX: currentDraggingArea.startX,
startY: currentDraggingArea.startY,
endX: currentDraggingArea.endX,
endY: currentDraggingArea.endY,
};
} else {
this._selectionRectTransition = null;
}
})
);
}
override pointerDown(e: PointerEventState): void {
this.interactivity?.dispatchEvent('pointerdown', e);
}
override pointerMove(e: PointerEventState) {
this.interactivity?.dispatchEvent('pointermove', e);
}
override pointerUp(e: PointerEventState) {
this.interactivity?.dispatchEvent('pointerup', e);
}
override unmounted(): void {}
}
declare module '@blocksuite/std/gfx' {
interface GfxToolsMap {
default: DefaultTool;
}
}

View File

@@ -0,0 +1,46 @@
import type { IVec } from '@blocksuite/global/gfx';
import type { PointerEventState } from '@blocksuite/std';
import type { Viewport } from '@blocksuite/std/gfx';
const PANNING_DISTANCE = 30;
export function calPanDelta(
viewport: Viewport,
e: PointerEventState,
edgeDistance = 20
): IVec | null {
// Get viewport edge
const { left, top } = viewport;
const { width, height } = viewport;
// Get pointer position
let { x, y } = e;
const { containerOffset } = e;
x += containerOffset.x;
y += containerOffset.y;
// Check if pointer is near viewport edge
const nearLeft = x < left + edgeDistance;
const nearRight = x > left + width - edgeDistance;
const nearTop = y < top + edgeDistance;
const nearBottom = y > top + height - edgeDistance;
// If pointer is not near viewport edge, return false
if (!(nearLeft || nearRight || nearTop || nearBottom)) return null;
// Calculate move delta
let deltaX = 0;
let deltaY = 0;
// Use PANNING_DISTANCE to limit the max delta, avoid panning too fast
if (nearLeft) {
deltaX = Math.max(-PANNING_DISTANCE, x - (left + edgeDistance));
} else if (nearRight) {
deltaX = Math.min(PANNING_DISTANCE, x - (left + width - edgeDistance));
}
if (nearTop) {
deltaY = Math.max(-PANNING_DISTANCE, y - (top + edgeDistance));
} else if (nearBottom) {
deltaY = Math.min(PANNING_DISTANCE, y - (top + height - edgeDistance));
}
return [deltaX, deltaY];
}

View File

@@ -1,137 +0,0 @@
import {
DEFAULT_NOTE_HEIGHT,
DEFAULT_NOTE_WIDTH,
NOTE_MIN_HEIGHT,
type NoteBlockModel,
NoteDisplayMode,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import type { NoteChildrenFlavour } from '@blocksuite/affine-shared/types';
import { handleNativeRangeAtPoint } from '@blocksuite/affine-shared/utils';
import { type IPoint, type Point, serializeXYWH } from '@blocksuite/global/gfx';
import type { BlockStdScope } from '@blocksuite/std';
import {
type GfxBlockElementModel,
GfxControllerIdentifier,
} from '@blocksuite/std/gfx';
import { DEFAULT_NOTE_OFFSET_X, DEFAULT_NOTE_OFFSET_Y } from '../consts';
import { EdgelessCRUDIdentifier } from '../extensions/crud-extension';
export function addNoteAtPoint(
std: BlockStdScope,
/**
* The point is in browser coordinate
*/
point: IPoint,
options: {
width?: number;
height?: number;
parentId?: string;
noteIndex?: number;
offsetX?: number;
offsetY?: number;
scale?: number;
} = {}
) {
const gfx = std.get(GfxControllerIdentifier);
const crud = std.get(EdgelessCRUDIdentifier);
const {
width = DEFAULT_NOTE_WIDTH,
height = DEFAULT_NOTE_HEIGHT,
offsetX = DEFAULT_NOTE_OFFSET_X,
offsetY = DEFAULT_NOTE_OFFSET_Y,
parentId = gfx.doc.root?.id,
noteIndex,
scale = 1,
} = options;
const [x, y] = gfx.viewport.toModelCoord(point.x, point.y);
const blockId = crud.addBlock(
'affine:note',
{
xywh: serializeXYWH(
x - offsetX * scale,
y - offsetY * scale,
width,
height
),
displayMode: NoteDisplayMode.EdgelessOnly,
},
parentId,
noteIndex
);
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
control: 'canvas:draw',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'note',
});
return blockId;
}
type NoteOptions = {
childFlavour: NoteChildrenFlavour;
childType: string | null;
collapse: boolean;
};
export function addNote(
std: BlockStdScope,
point: Point,
options: NoteOptions,
width = DEFAULT_NOTE_WIDTH,
height = DEFAULT_NOTE_HEIGHT
) {
const noteId = addNoteAtPoint(std, point, {
width,
height,
});
const gfx = std.get(GfxControllerIdentifier);
const doc = std.store;
const blockId = doc.addBlock(
options.childFlavour,
{ type: options.childType },
noteId
);
if (options.collapse && height > NOTE_MIN_HEIGHT) {
const note = doc.getModelById(noteId) as NoteBlockModel;
doc.updateBlock(note, () => {
note.props.edgeless.collapse = true;
note.props.edgeless.collapsedHeight = height;
});
}
gfx.tool.setTool(
// @ts-expect-error FIXME: resolve after gfx tool refactor
'default'
);
// Wait for edgelessTool updated
requestAnimationFrame(() => {
const blocks =
(doc.root?.children.filter(
child => child.flavour === 'affine:note'
) as GfxBlockElementModel[]) ?? [];
const element = blocks.find(b => b.id === noteId);
if (element) {
gfx.selection.set({
elements: [element.id],
editing: true,
});
// Waiting dom updated, `note mask` is removed
if (blockId) {
focusTextModel(gfx.std, blockId);
} else {
// Cannot reuse `handleNativeRangeClick` directly here,
// since `retargetClick` will re-target to pervious editor
handleNativeRangeAtPoint(point.x, point.y);
}
}
});
}

View File

@@ -33,7 +33,6 @@ export function normalizeWheelDeltaY(delta: number, zoom = 1) {
return newZoom;
}
export { addNote, addNoteAtPoint } from './add-note';
export { getBgGridGap } from './get-bg-grip-gap';
export { getLastPropsKey } from './get-last-props-key';
export * from './get-surface-block';

View File

@@ -12,6 +12,7 @@ import {
EditPropsMiddlewareBuilder,
} from './extensions';
import { ExportManagerExtension } from './extensions/export-manager/export-manager';
import { DefaultTool } from './tool/default-tool';
export class SurfaceViewExtension extends ViewExtensionProvider {
override name = 'affine-surface-block';
@@ -30,6 +31,7 @@ export class SurfaceViewExtension extends ViewExtensionProvider {
ExportManagerExtension,
]);
if (this.isEdgeless(context.scope)) {
context.register(DefaultTool);
context.register(
BlockViewExtension('affine:surface', literal`affine-surface`)
);