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,10 +1,12 @@
import { DefaultTool } from '@blocksuite/affine-block-surface';
import { QuickToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
import { HandIcon, SelectIcon } from '@blocksuite/icons/lit';
import type { GfxToolsFullOptionValue } from '@blocksuite/std/gfx';
import { effect } from '@preact/signals-core';
import { css, html, LitElement } from 'lit';
import { query } from 'lit/decorators.js';
import { PanTool } from '../tools';
export class EdgelessDefaultToolButton extends QuickToolMixin(LitElement) {
static override styles = css`
.current-icon {
@@ -17,19 +19,19 @@ export class EdgelessDefaultToolButton extends QuickToolMixin(LitElement) {
}
`;
override type: GfxToolsFullOptionValue['type'][] = ['default', 'pan'];
override type = [DefaultTool, PanTool];
private _changeTool() {
if (this.toolbar.activePopper) {
// click manually always closes the popper
this.toolbar.activePopper.dispose();
}
const type = this.edgelessTool?.type;
const type = this.edgelessTool?.toolType?.toolName;
if (type !== 'default' && type !== 'pan') {
if (localStorage.defaultTool === 'default') {
this.setEdgelessTool('default');
this.setEdgelessTool(DefaultTool);
} else if (localStorage.defaultTool === 'pan') {
this.setEdgelessTool('pan', { panning: false });
this.setEdgelessTool(PanTool, { panning: false });
}
return;
}
@@ -37,9 +39,9 @@ export class EdgelessDefaultToolButton extends QuickToolMixin(LitElement) {
// wait for animation to finish
setTimeout(() => {
if (type === 'default') {
this.setEdgelessTool('pan', { panning: false });
this.setEdgelessTool(PanTool, { panning: false });
} else if (type === 'pan') {
this.setEdgelessTool('default');
this.setEdgelessTool(DefaultTool);
}
this._fadeIn();
}, 100);
@@ -71,7 +73,7 @@ export class EdgelessDefaultToolButton extends QuickToolMixin(LitElement) {
}
override render() {
const type = this.edgelessTool?.type;
const type = this.edgelessTool?.toolType?.toolName;
const { active } = this;
const tipInfo =
type === 'pan'

View File

@@ -1,424 +0,0 @@
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 '../utils/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

@@ -1,3 +1,2 @@
export * from './default-tool.js';
export * from './empty-tool.js';
export * from './pan-tool.js';

View File

@@ -56,11 +56,14 @@ export class PanTool extends BaseTool<PanToolOption> {
const selection = this.gfx.selection.surfaceSelections;
const currentTool = this.controller.currentToolOption$.peek();
const restoreToPrevious = () => {
this.controller.setTool(currentTool);
this.gfx.selection.set(selection);
const { toolType, options } = currentTool;
if (toolType && options) {
this.controller.setTool(toolType, options);
this.gfx.selection.set(selection);
}
};
this.controller.setTool('pan', {
this.controller.setTool(PanTool, {
panning: true,
});

View File

@@ -1,46 +0,0 @@
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

@@ -7,7 +7,7 @@ import { effects } from './effects';
import { defaultQuickTool } from './quick-tool/quick-tool';
import { SnapExtension } from './snap/snap-manager';
import { SnapOverlay } from './snap/snap-overlay';
import { DefaultTool, EmptyTool, PanTool } from './tools';
import { EmptyTool, PanTool } from './tools';
export class PointerViewExtension extends ViewExtensionProvider {
override name = 'affine-pointer-gfx';
@@ -20,7 +20,6 @@ export class PointerViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) {
super.setup(context);
context.register(EmptyTool);
context.register(DefaultTool);
context.register(PanTool);
if (this.isEdgeless(context.scope)) {
context.register(defaultQuickTool);