refactor(editor): unify directories naming (#11516)

**Directory Structure Changes**

- Renamed multiple block-related directories by removing the "block-" prefix:
  - `block-attachment` → `attachment`
  - `block-bookmark` → `bookmark`
  - `block-callout` → `callout`
  - `block-code` → `code`
  - `block-data-view` → `data-view`
  - `block-database` → `database`
  - `block-divider` → `divider`
  - `block-edgeless-text` → `edgeless-text`
  - `block-embed` → `embed`
This commit is contained in:
Saul-Mirone
2025-04-07 12:34:40 +00:00
parent e1bd2047c4
commit 1f45cc5dec
893 changed files with 439 additions and 460 deletions
@@ -0,0 +1,441 @@
import { type Color, ColorScheme } from '@blocksuite/affine-model';
import { requestConnectedFrame } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { IBound } from '@blocksuite/global/gfx';
import { getBoundWithRotation, intersects } from '@blocksuite/global/gfx';
import type { BlockStdScope } from '@blocksuite/std';
import type {
GridManager,
LayerManager,
SurfaceBlockModel,
Viewport,
} from '@blocksuite/std/gfx';
import last from 'lodash-es/last';
import { Subject } from 'rxjs';
import type { SurfaceElementModel } from '../element-model/base.js';
import { ElementRendererIdentifier } from '../extensions/element-renderer.js';
import { RoughCanvas } from '../utils/rough/canvas.js';
import type { ElementRenderer } from './elements/index.js';
import type { Overlay } from './overlay.js';
type EnvProvider = {
generateColorProperty: (color: Color, fallback?: Color) => string;
getColorScheme: () => ColorScheme;
getColorValue: (color: Color, fallback?: Color, real?: boolean) => string;
getPropertyValue: (property: string) => string;
selectedElements?: () => string[];
};
type RendererOptions = {
std: BlockStdScope;
viewport: Viewport;
layerManager: LayerManager;
provider?: Partial<EnvProvider>;
enableStackingCanvas?: boolean;
onStackingCanvasCreated?: (canvas: HTMLCanvasElement) => void;
gridManager: GridManager;
surfaceModel: SurfaceBlockModel;
};
export class CanvasRenderer {
private _container!: HTMLElement;
private readonly _disposables = new DisposableGroup();
private readonly _overlays = new Set<Overlay>();
private _refreshRafId: number | null = null;
private _stackingCanvas: HTMLCanvasElement[] = [];
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
std: BlockStdScope;
grid: GridManager;
layerManager: LayerManager;
provider: Partial<EnvProvider>;
stackingCanvasUpdated = new Subject<{
canvases: HTMLCanvasElement[];
added: HTMLCanvasElement[];
removed: HTMLCanvasElement[];
}>();
viewport: Viewport;
get stackingCanvas() {
return this._stackingCanvas;
}
constructor(options: RendererOptions) {
const canvas = document.createElement('canvas');
this.canvas = canvas;
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
this.std = options.std;
this.viewport = options.viewport;
this.layerManager = options.layerManager;
this.grid = options.gridManager;
this.provider = options.provider ?? {};
this._initViewport();
options.enableStackingCanvas = options.enableStackingCanvas ?? false;
if (options.enableStackingCanvas) {
this._initStackingCanvas(options.onStackingCanvasCreated);
}
this._watchSurface(options.surfaceModel);
}
/**
* Specifying the actual size gives better results and more consistent behavior across browsers.
*
* Make sure the main canvas and the offscreen canvas or layer canvas are the same size.
*
* It is not recommended to set width and height to 100%.
*/
private _canvasSizeUpdater(dpr = window.devicePixelRatio) {
const { width, height } = this.viewport;
const actualWidth = Math.ceil(width * dpr);
const actualHeight = Math.ceil(height * dpr);
return {
filter({ width, height }: HTMLCanvasElement) {
return width !== actualWidth || height !== actualHeight;
},
update(canvas: HTMLCanvasElement) {
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
canvas.width = actualWidth;
canvas.height = actualHeight;
},
};
}
private _initStackingCanvas(onCreated?: (canvas: HTMLCanvasElement) => void) {
const layer = this.layerManager;
const updateStackingCanvasSize = (canvases: HTMLCanvasElement[]) => {
this._stackingCanvas = canvases;
const sizeUpdater = this._canvasSizeUpdater();
canvases.filter(sizeUpdater.filter).forEach(sizeUpdater.update);
};
const updateStackingCanvas = () => {
/**
* we already have a main canvas, so the last layer should be skipped
*/
const canvasLayers = layer.getCanvasLayers().slice(0, -1);
const canvases = [];
const currentCanvases = this._stackingCanvas;
const lastLayer = last(this.layerManager.layers);
const maximumZIndex = lastLayer
? lastLayer.zIndex + lastLayer.elements.length + 1
: 1;
this.canvas.style.zIndex = maximumZIndex.toString();
for (let i = 0; i < canvasLayers.length; ++i) {
const layer = canvasLayers[i];
const created = i < currentCanvases.length;
const canvas = created
? currentCanvases[i]
: document.createElement('canvas');
if (!created) {
onCreated?.(canvas);
}
canvas.dataset.layerId = `[${layer.indexes[0]}--${layer.indexes[1]}]`;
canvas.style.zIndex = layer.zIndex.toString();
canvases.push(canvas);
}
this._stackingCanvas = canvases;
updateStackingCanvasSize(canvases);
if (currentCanvases.length !== canvases.length) {
const diff = canvases.length - currentCanvases.length;
const payload: {
canvases: HTMLCanvasElement[];
removed: HTMLCanvasElement[];
added: HTMLCanvasElement[];
} = {
canvases,
removed: [],
added: [],
};
if (diff > 0) {
payload.added = canvases.slice(-diff);
} else {
payload.removed = currentCanvases.slice(diff);
}
this.stackingCanvasUpdated.next(payload);
}
this.refresh();
};
this._disposables.add(
this.layerManager.slots.layerUpdated.subscribe(() => {
updateStackingCanvas();
})
);
updateStackingCanvas();
}
private _initViewport() {
let sizeUpdatedRafId: number | null = null;
this._disposables.add(
this.viewport.viewportUpdated.subscribe(() => {
this.refresh();
})
);
this._disposables.add(
this.viewport.sizeUpdated.subscribe(() => {
if (sizeUpdatedRafId) return;
sizeUpdatedRafId = requestConnectedFrame(() => {
sizeUpdatedRafId = null;
this._resetSize();
this._render();
this.refresh();
}, this._container);
})
);
}
private _render() {
const { viewportBounds, zoom } = this.viewport;
const { ctx } = this;
const dpr = window.devicePixelRatio;
const scale = zoom * dpr;
const matrix = new DOMMatrix().scaleSelf(scale);
/**
* if a layer does not have a corresponding canvas
* its element will be add to this array and drawing on the
* main canvas
*/
let fallbackElement: SurfaceElementModel[] = [];
this.layerManager.getCanvasLayers().forEach((layer, idx) => {
if (!this._stackingCanvas[idx]) {
fallbackElement = fallbackElement.concat(layer.elements);
return;
}
const canvas = this._stackingCanvas[idx];
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
const rc = new RoughCanvas(ctx.canvas);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.setTransform(matrix);
this._renderByBound(ctx, matrix, rc, viewportBounds, layer.elements);
});
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.save();
ctx.setTransform(matrix);
this._renderByBound(
ctx,
matrix,
new RoughCanvas(ctx.canvas),
viewportBounds,
fallbackElement,
true
);
}
private _renderByBound(
ctx: CanvasRenderingContext2D | null,
matrix: DOMMatrix,
rc: RoughCanvas,
bound: IBound,
surfaceElements?: SurfaceElementModel[],
overLay: boolean = false
) {
if (!ctx) return;
const elements =
surfaceElements ??
(this.grid.search(bound, {
filter: ['canvas', 'local'],
}) as SurfaceElementModel[]);
for (const element of elements) {
const display = (element.display ?? true) && !element.hidden;
if (display && intersects(getBoundWithRotation(element), bound)) {
const renderFn = this.std.getOptional<ElementRenderer>(
ElementRendererIdentifier(element.type)
);
if (!renderFn) {
console.warn(`Cannot find renderer for ${element.type}`);
continue;
}
ctx.save();
ctx.globalAlpha = element.opacity ?? 1;
const dx = element.x - bound.x;
const dy = element.y - bound.y;
renderFn(element, ctx, matrix.translate(dx, dy), this, rc, bound);
ctx.restore();
}
}
if (overLay) {
for (const overlay of this._overlays) {
ctx.save();
ctx.translate(-bound.x, -bound.y);
overlay.render(ctx, rc);
ctx.restore();
}
}
ctx.restore();
}
private _resetSize() {
const sizeUpdater = this._canvasSizeUpdater();
sizeUpdater.update(this.canvas);
this._stackingCanvas.forEach(sizeUpdater.update);
this.refresh();
}
private _watchSurface(surfaceModel: SurfaceBlockModel) {
this._disposables.add(
surfaceModel.elementAdded.subscribe(() => this.refresh())
);
this._disposables.add(
surfaceModel.elementRemoved.subscribe(() => this.refresh())
);
this._disposables.add(
surfaceModel.localElementAdded.subscribe(() => this.refresh())
);
this._disposables.add(
surfaceModel.localElementDeleted.subscribe(() => this.refresh())
);
this._disposables.add(
surfaceModel.localElementUpdated.subscribe(() => this.refresh())
);
this._disposables.add(
surfaceModel.elementUpdated.subscribe(payload => {
// ignore externalXYWH update cause it's updated by the renderer
if (payload.props['externalXYWH']) return;
this.refresh();
})
);
}
addOverlay(overlay: Overlay) {
overlay.setRenderer(this);
this._overlays.add(overlay);
this.refresh();
}
/**
* Used to attach main canvas, main canvas will always exist
* @param container
*/
attach(container: HTMLElement) {
this._container = container;
container.append(this.canvas);
this._resetSize();
this.refresh();
}
dispose(): void {
this._overlays.forEach(overlay => overlay.dispose());
this._overlays.clear();
this._disposables.dispose();
}
generateColorProperty(color: Color, fallback?: Color) {
return (
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
);
}
getCanvasByBound(
bound: IBound = this.viewport.viewportBounds,
surfaceElements?: SurfaceElementModel[],
canvas?: HTMLCanvasElement,
clearBeforeDrawing?: boolean,
withZoom?: boolean
): HTMLCanvasElement {
canvas = canvas || document.createElement('canvas');
const dpr = window.devicePixelRatio || 1;
if (canvas.width !== bound.w * dpr) canvas.width = bound.w * dpr;
if (canvas.height !== bound.h * dpr) canvas.height = bound.h * dpr;
canvas.style.width = `${bound.w}px`;
canvas.style.height = `${bound.h}px`;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
const matrix = new DOMMatrix().scaleSelf(
withZoom ? dpr * this.viewport.zoom : dpr
);
const rc = new RoughCanvas(canvas);
if (clearBeforeDrawing) ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(matrix);
this._renderByBound(ctx, matrix, rc, bound, surfaceElements);
return canvas;
}
getColorScheme() {
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
}
getColorValue(color: Color, fallback?: Color, real?: boolean) {
return (
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
);
}
getPropertyValue(property: string) {
return this.provider.getPropertyValue?.(property) ?? '';
}
refresh() {
if (this._refreshRafId !== null) return;
this._refreshRafId = requestConnectedFrame(() => {
this._refreshRafId = null;
this._render();
}, this._container);
}
removeOverlay(overlay: Overlay) {
if (!this._overlays.has(overlay)) {
return;
}
overlay.setRenderer(null);
this._overlays.delete(overlay);
this.refresh();
}
}
@@ -0,0 +1,23 @@
import type { IBound } from '@blocksuite/global/gfx';
import type {
GfxLocalElementModel,
GfxPrimitiveElementModel,
} from '@blocksuite/std/gfx';
import type { RoughCanvas } from '../../index.js';
import type { CanvasRenderer } from '../canvas-renderer.js';
export type ElementRenderer<
T extends
| GfxPrimitiveElementModel
| GfxLocalElementModel = GfxPrimitiveElementModel,
> = (
model: T,
ctx: CanvasRenderingContext2D,
matrix: DOMMatrix,
renderer: CanvasRenderer,
rc: RoughCanvas,
viewportBound: IBound
) => void;
export const elementRendererExtensions = [];
@@ -0,0 +1,6 @@
import type {
ShapeElementModel,
TextElementModel,
} from '@blocksuite/affine-model';
export type CanvasElementWithText = ShapeElementModel | TextElementModel;
@@ -0,0 +1,55 @@
import { type Container, createIdentifier } from '@blocksuite/global/di';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import {
type GfxController,
GfxControllerIdentifier,
} from '@blocksuite/std/gfx';
import { Extension } from '@blocksuite/store';
import type { RoughCanvas } from '../utils/rough/canvas.js';
import type { CanvasRenderer } from './canvas-renderer.js';
/**
* An overlay is a layer covered on top of elements,
* can be used for rendering non-CRDT state indicators.
*/
export abstract class Overlay extends Extension {
static overlayName: string = '';
protected _renderer: CanvasRenderer | null = null;
constructor(protected gfx: GfxController) {
super();
}
static override setup(di: Container): void {
if (!this.overlayName) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
`The overlay constructor '${this.name}' should have a static 'overlayName' property.`
);
}
di.addImpl(OverlayIdentifier(this.overlayName), this, [
GfxControllerIdentifier,
]);
}
clear() {}
dispose() {}
refresh() {
if (this._renderer) {
this._renderer.refresh();
}
}
abstract render(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void;
setRenderer(renderer: CanvasRenderer | null) {
this._renderer = renderer;
}
}
export const OverlayIdentifier = createIdentifier<Overlay>('Overlay');
@@ -0,0 +1,42 @@
import { DisposableGroup } from '@blocksuite/global/disposable';
import { noop } from '@blocksuite/global/utils';
import type { GfxController } from '@blocksuite/std/gfx';
import type { RoughCanvas } from '../utils/rough/canvas';
import { Overlay } from './overlay';
export class ToolOverlay extends Overlay {
protected disposables = new DisposableGroup();
globalAlpha: number;
x: number;
y: number;
constructor(gfx: GfxController) {
super(gfx);
this.x = 0;
this.y = 0;
this.globalAlpha = 0;
this.gfx = gfx;
this.disposables.add(
this.gfx.viewport.viewportUpdated.subscribe(() => {
// when viewport is updated, we should keep the overlay in the same position
// to get last mouse position and convert it to model coordinates
const pos = this.gfx.tool.lastMousePos$.value;
const [x, y] = this.gfx.viewport.toModelCoord(pos.x, pos.y);
this.x = x;
this.y = y;
})
);
}
override dispose(): void {
this.disposables.dispose();
}
render(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void {
noop([ctx, rc]);
}
}