Skip to content

Commit

Permalink
feat(edgeless): add views and event support for canvas elements (#8882)
Browse files Browse the repository at this point in the history
### **Introducing Views for Canvas Elements**

This PR introduces the concept of views for canvas elements. Each canvas element, including those local element added via `addLocalElement`, now has an associated view instance. Views handle logic related to the view layer, such as binding mouse events to elements.

```typescript
const modelView = std.gfx.view.get(model);

view.on('pointerenter', () => {
  console.log('mouse entered the element.');
});
```

You don’t need to manually instantiate views for elements. They are automatically managed by the `ViewManager`.

### **Custom View Classes**

By default, each element's view is an instance of the `GfxElementModelView` class. However, you can define custom view classes for specific element types

```typescript
import { GfxElementModelView } from '@blocksuite/block-std/gfx';

export class ShapeView extends GfxElementModelView {
  // Required static property: the model type this view is associated with
  static override type = 'shape';

  onCreated() {
    this.on('click', () => {
      enterShapeTextEditor(this.model);
    });
  }
}

// surface-spec.ts
export surfaceSpec = [
  //... other extensions
  ShapeView
];
```

### **Supported Events**

The following mouse events are currently supported by views:

- `click`
- `dblclick`
- `pointerdown`
- `pointerenter`
- `pointerleave`
- `pointermove`
- `pointerup`
  • Loading branch information
doouding committed Dec 18, 2024
1 parent 10a4437 commit 91118b8
Show file tree
Hide file tree
Showing 12 changed files with 467 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { PointerEventState } from '@blocksuite/block-std';
import type { GfxElementModelView } from '@blocksuite/block-std/gfx';

import { Bound, last } from '@blocksuite/global/utils';

import { DefaultModeDragType, DefaultToolExt } from './ext.js';

export class CanvasElementEventExt extends DefaultToolExt {
private _currentStackedElm: GfxElementModelView[] = [];

override supportedDragTypes: DefaultModeDragType[] = [
DefaultModeDragType.None,
];

private _callInReverseOrder(
callback: (view: GfxElementModelView) => void,
arr = this._currentStackedElm
) {
for (let i = arr.length - 1; i >= 0; i--) {
const view = arr[i];

callback(view);
}
}

override click(_evt: PointerEventState): void {
last(this._currentStackedElm)?.dispatch('click', _evt);
}

override dblClick(_evt: PointerEventState): void {
last(this._currentStackedElm)?.dispatch('dblclick', _evt);
}

override pointerDown(_evt: PointerEventState): void {
last(this._currentStackedElm)?.dispatch('pointerdown', _evt);
}

override pointerMove(_evt: PointerEventState): void {
const [x, y] = this.gfx.viewport.toModelCoord(_evt.x, _evt.y);
const hoveredElmViews = this.gfx.grid
.search(new Bound(x, y, 1, 1), {
filter: ['canvas', 'local'],
})
.map(model => this.gfx.view.get(model)) as GfxElementModelView[];
const currentStackedViews = new Set(this._currentStackedElm);
const visited = new Set<GfxElementModelView>();

this._callInReverseOrder(view => {
if (currentStackedViews.has(view)) {
visited.add(view);
view.dispatch('pointermove', _evt);
} else {
view.dispatch('pointerenter', _evt);
}
}, hoveredElmViews);
this._callInReverseOrder(
view => !visited.has(view) && view.dispatch('pointerleave', _evt)
);
this._currentStackedElm = hoveredElmViews;
}

override pointerUp(_evt: PointerEventState): void {
last(this._currentStackedElm)?.dispatch('pointerup', _evt);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export type DragState = {
};

export class DefaultToolExt {
readonly supportedDragTypes: DefaultModeDragType[] = [];

get gfx() {
return this.defaultTool.gfx;
}
Expand All @@ -37,6 +39,10 @@ export class DefaultToolExt {

constructor(protected defaultTool: DefaultTool) {}

click(_evt: PointerEventState) {}

dblClick(_evt: PointerEventState) {}

initDrag(_: DragState): {
dragStart?: (evt: PointerEventState) => void;
dragMove?: (evt: PointerEventState) => void;
Expand All @@ -47,5 +53,11 @@ export class DefaultToolExt {

mounted() {}

pointerDown(_evt: PointerEventState) {}

pointerMove(_evt: PointerEventState) {}

pointerUp(_evt: PointerEventState) {}

unmounted() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type DragMindMapCtx = {
export class MindMapExt extends DefaultToolExt {
private _responseAreaUpdated = new Set<MindmapElementModel>();

override supportedDragTypes: DefaultModeDragType[] = [
DefaultModeDragType.ContentMoving,
];

private get _indicatorOverlay() {
return this.std.getOptional(
OverlayIdentifier('mindmap-indicator')
Expand Down
27 changes: 25 additions & 2 deletions packages/blocks/src/root-block/edgeless/gfx-tool/default-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
mountTextElementEditor,
} from '../utils/text.js';
import { fitToScreen } from '../utils/viewport.js';
import { CanvasElementEventExt } from './default-tool-ext/event-ext.js';
import { DefaultModeDragType } from './default-tool-ext/ext.js';
import { MindMapExt } from './default-tool-ext/mind-map-ext/mind-map-ext.js';

Expand Down Expand Up @@ -242,6 +243,12 @@ export class DefaultTool extends BaseTool {
) as EdgelessFrameManager;
}

private get _supportedExts() {
return this._exts.filter(ext =>
ext.supportedDragTypes.includes(this.dragType)
);
}

/**
* Get the end position of the dragging area in the model coordinate
*/
Expand Down Expand Up @@ -586,7 +593,7 @@ export class DefaultTool extends BaseTool {
event,
};

this._extHandlers = this._exts.map(ext => ext.initDrag(ctx));
this._extHandlers = this._supportedExts.map(ext => ext.initDrag(ctx));
this._selectedBounds = this._toBeMoved.map(element =>
Bound.deserialize(element.xywh)
);
Expand Down Expand Up @@ -742,6 +749,7 @@ export class DefaultTool extends BaseTool {
}

this._isDoubleClickedOnMask = false;
this._supportedExts.forEach(ext => ext.click?.(e));
}

override deactivate() {
Expand Down Expand Up @@ -820,6 +828,8 @@ export class DefaultTool extends BaseTool {
}
}

this._supportedExts.forEach(ext => ext.click?.(e));

if (
e.raw.target &&
e.raw.target instanceof HTMLElement &&
Expand Down Expand Up @@ -1003,14 +1013,21 @@ export class DefaultTool extends BaseTool {
})
);

this._exts = [MindMapExt].map(constructor => new constructor(this));
this._exts = [MindMapExt, CanvasElementEventExt].map(
constructor => new constructor(this)
);
this._exts.forEach(ext => ext.mounted());
}

override pointerDown(e: PointerEventState): void {
this._supportedExts.forEach(ext => ext.pointerDown(e));
}

override pointerMove(e: PointerEventState) {
const hovered = this._pick(e.x, e.y, {
hitThreshold: 10,
});

if (
isFrameBlock(hovered) &&
hovered.externalBound?.isPointInBound(
Expand All @@ -1021,6 +1038,12 @@ export class DefaultTool extends BaseTool {
} else {
this.frameOverlay.clear();
}

this._supportedExts.forEach(ext => ext.pointerMove(e));
}

override pointerUp(e: PointerEventState) {
this._supportedExts.forEach(ext => ext.pointerUp(e));
}

override tripleClick() {
Expand Down
30 changes: 19 additions & 11 deletions packages/framework/block-std/src/gfx/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ export class GridManager {
}
}

private _searchExternal(bound: IBound, strict = false): Set<GfxModel> {
private _searchExternal(
bound: IBound,
options: { filterFunc: FilterFunc; strict: boolean }
): Set<GfxModel> {
const [minRow, maxRow, minCol, maxCol] = rangeFromBound(bound);
const results = new Set<GfxModel>();
const b = Bound.from(bound);
Expand All @@ -147,8 +150,9 @@ export class GridManager {
for (const element of gridElements) {
const externalBound = element.externalBound;
if (
options.filterFunc(element) &&
externalBound &&
(strict
(options.strict
? b.contains(externalBound)
: intersects(externalBound, bound))
) {
Expand All @@ -161,13 +165,14 @@ export class GridManager {
return results;
}

private _toFilterFuncs(filters: (keyof typeof typeFilters | FilterFunc)[]) {
private _toFilterFunc(filters: (keyof typeof typeFilters | FilterFunc)[]) {
const filterFuncs: FilterFunc[] = filters.map(filter => {
if (typeof filter === 'function') {
return filter;
}
return typeFilters[filter];
});

return (model: GfxModel | GfxLocalElementModel) =>
filterFuncs.some(filter => filter(model));
}
Expand Down Expand Up @@ -300,25 +305,28 @@ export class GridManager {
| (GfxModel | GfxLocalElementModel)[]
| Set<GfxModel | GfxLocalElementModel> {
const strict = options.strict ?? false;
const results: Set<GfxModel | GfxLocalElementModel> = this._searchExternal(
bound,
strict
);
const [minRow, maxRow, minCol, maxCol] = rangeFromBound(bound);
const b = Bound.from(bound);
const returnSet = options.useSet ?? false;
const filter =
const filterFunc =
(Array.isArray(options.filter)
? this._toFilterFuncs(options.filter)
: options.filter) ?? this._toFilterFuncs(['canvas', 'block']);
? this._toFilterFunc(options.filter)
: options.filter) ?? this._toFilterFunc(['canvas', 'block']);
const results: Set<GfxModel | GfxLocalElementModel> = this._searchExternal(
bound,
{
filterFunc,
strict,
}
);

for (let i = minRow; i <= maxRow; i++) {
for (let j = minCol; j <= maxCol; j++) {
const gridElements = this._getGrid(i, j);
if (!gridElements) continue;
for (const element of gridElements) {
if (
filter(element) &&
filterFunc(element) &&
(strict
? b.contains(element.elementBound)
: intersects(element.elementBound, b))
Expand Down
7 changes: 7 additions & 0 deletions packages/framework/block-std/src/gfx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,14 @@ export {
type GfxToolsMap,
type GfxToolsOption,
} from './tool/tool.js';

export { MouseButton, ToolController } from './tool/tool-controller.js';
export {
type EventsHandlerMap,
GfxElementModelView,
type SupportedEvent,
} from './view/view.js';
export { ViewManager } from './view/view-manager.js';
export * from './viewport.js';
export { GfxViewportElement } from './viewport-element.js';
export { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { IVec, SerializedXYWH, XYWH } from '@blocksuite/global/utils';

import {
type IVec,
type SerializedXYWH,
Slot,
type XYWH,
} from '@blocksuite/global/utils';
import {
Bound,
deserializeXYWH,
Expand Down Expand Up @@ -86,6 +90,8 @@ export abstract class GfxPrimitiveElementModel<

protected _stashed: Map<keyof Props | string, unknown>;

propsUpdated = new Slot<{ key: string }>();

abstract rotate: number;

surface!: SurfaceBlockModel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,12 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
element.get('id') as string,
element,
{
onChange: payload => this.elementUpdated.emit(payload),
onChange: payload => {
this.elementUpdated.emit(payload);
Object.keys(payload.props).forEach(key => {
model.model.propsUpdated.emit({ key });
});
},
skipFieldInit: true,
}
);
Expand Down Expand Up @@ -321,7 +326,12 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
val.get('id') as string,
val,
{
onChange: payload => this.elementUpdated.emit(payload),
onChange: payload => {
this.elementUpdated.emit(payload),
Object.keys(payload.props).forEach(key => {
model.model.propsUpdated.emit({ key });
});
},
skipFieldInit: true,
}
);
Expand Down Expand Up @@ -443,7 +453,12 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
props.id = id;

const elementModel = this._createElementFromProps(props, {
onChange: payload => this.elementUpdated.emit(payload),
onChange: payload => {
this.elementUpdated.emit(payload);
Object.keys(payload.props).forEach(key => {
elementModel.model.propsUpdated.emit({ key });
});
},
});

this._elementModels.set(id, elementModel);
Expand Down
3 changes: 2 additions & 1 deletion packages/framework/block-std/src/gfx/surface-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ export class SurfaceMiddlewareExtension extends LifeCycleWatcher {
this.std.provider.getAll(SurfaceMiddlewareBuilderIdentifier).values()
);

onSurfaceAdded(this.std.doc, surface => {
const dispose = onSurfaceAdded(this.std.doc, surface => {
if (surface) {
surface.applyMiddlewares(builders.map(builder => builder.middleware));
queueMicrotask(() => dispose());
}
});
}
Expand Down
Loading

0 comments on commit 91118b8

Please sign in to comment.