diff --git a/blocksuite/affine/blocks/block-list/package.json b/blocksuite/affine/blocks/block-list/package.json index aa80b49c35..b0750b7247 100644 --- a/blocksuite/affine/blocks/block-list/package.json +++ b/blocksuite/affine/blocks/block-list/package.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "@blocksuite/affine-components": "workspace:*", + "@blocksuite/affine-gfx-turbo-renderer": "workspace:*", "@blocksuite/affine-inline-preset": "workspace:*", "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-rich-text": "workspace:*", @@ -34,7 +35,8 @@ }, "exports": { ".": "./src/index.ts", - "./effects": "./src/effects.ts" + "./effects": "./src/effects.ts", + "./turbo-painter": "./src/turbo/list-painter.worker.ts" }, "files": [ "src", diff --git a/blocksuite/affine/blocks/block-list/src/index.ts b/blocksuite/affine/blocks/block-list/src/index.ts index 7f18d6d6a4..f8aac911ce 100644 --- a/blocksuite/affine/blocks/block-list/src/index.ts +++ b/blocksuite/affine/blocks/block-list/src/index.ts @@ -3,3 +3,5 @@ export * from './commands'; export { correctNumberedListsOrderToPrev } from './commands/utils'; export * from './list-block.js'; export * from './list-spec.js'; +export * from './turbo/list-layout-handler'; +export * from './turbo/list-painter.worker'; diff --git a/blocksuite/affine/blocks/block-list/src/turbo/list-layout-handler.ts b/blocksuite/affine/blocks/block-list/src/turbo/list-layout-handler.ts new file mode 100644 index 0000000000..b6aff940b4 --- /dev/null +++ b/blocksuite/affine/blocks/block-list/src/turbo/list-layout-handler.ts @@ -0,0 +1,145 @@ +import type { Rect } from '@blocksuite/affine-gfx-turbo-renderer'; +import { + BlockLayoutHandlerExtension, + BlockLayoutHandlersIdentifier, + getSentenceRects, + segmentSentences, +} from '@blocksuite/affine-gfx-turbo-renderer'; +import type { Container } from '@blocksuite/global/di'; +import type { GfxBlockComponent } from '@blocksuite/std'; +import { clientToModelCoord } from '@blocksuite/std/gfx'; + +import type { ListLayout } from './list-painter.worker'; + +export class ListLayoutHandlerExtension extends BlockLayoutHandlerExtension { + readonly blockType = 'affine:list'; + + static override setup(di: Container) { + di.addImpl( + BlockLayoutHandlersIdentifier('list'), + ListLayoutHandlerExtension + ); + } + + queryLayout(component: GfxBlockComponent): ListLayout | null { + // Select all list items within this list block + const listItemSelector = + '.affine-list-block-container .affine-list-rich-text-wrapper [data-v-text="true"]'; + const listItemNodes = component.querySelectorAll(listItemSelector); + + if (listItemNodes.length === 0) return null; + + const viewportRecord = component.gfx.viewport.deserializeRecord( + component.dataset.viewportState + ); + + if (!viewportRecord) return null; + + const { zoom, viewScale } = viewportRecord; + const list: ListLayout = { + type: 'affine:list', + items: [], + }; + + listItemNodes.forEach(listItemNode => { + const listItemWrapper = listItemNode.closest( + '.affine-list-rich-text-wrapper' + ); + if (!listItemWrapper) return; + + // Determine list item type based on class + let itemType: 'bulleted' | 'numbered' | 'todo' | 'toggle' = 'bulleted'; + let checked = false; + let collapsed = false; + let prefix = ''; + + if (listItemWrapper.classList.contains('affine-list--checked')) { + checked = true; + } + + const parentListBlock = listItemWrapper.closest( + '.affine-list-block-container' + )?.parentElement; + if (parentListBlock) { + if (parentListBlock.dataset.listType === 'numbered') { + itemType = 'numbered'; + const orderVal = parentListBlock.dataset.listOrder; + if (orderVal) { + prefix = orderVal + '.'; + } + } else if (parentListBlock.dataset.listType === 'todo') { + itemType = 'todo'; + } else if (parentListBlock.dataset.listType === 'toggle') { + itemType = 'toggle'; + collapsed = parentListBlock.dataset.collapsed === 'true'; + } else { + itemType = 'bulleted'; + } + } + + const computedStyle = window.getComputedStyle(listItemNode); + const fontSizeStr = computedStyle.fontSize; + const fontSize = parseInt(fontSizeStr); + + const sentences = segmentSentences(listItemNode.textContent || ''); + const sentenceLayouts = sentences.map(sentence => { + const sentenceRects = getSentenceRects(listItemNode, sentence); + return { + text: sentence, + rects: sentenceRects.map(({ text, rect }) => { + const [modelX, modelY] = clientToModelCoord(viewportRecord, [ + rect.x, + rect.y, + ]); + return { + text, + rect: { + x: modelX, + y: modelY, + w: rect.w / zoom / viewScale, + h: rect.h / zoom / viewScale, + }, + }; + }), + fontSize, + type: itemType, + prefix, + checked, + collapsed, + }; + }); + + list.items.push(...sentenceLayouts); + }); + + return list; + } + + calculateBound(layout: ListLayout) { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + layout.items.forEach(item => { + item.rects.forEach(r => { + minX = Math.min(minX, r.rect.x); + minY = Math.min(minY, r.rect.y); + maxX = Math.max(maxX, r.rect.x + r.rect.w); + maxY = Math.max(maxY, r.rect.y + r.rect.h); + }); + }); + + const rect: Rect = { + x: minX, + y: minY, + w: maxX - minX, + h: maxY - minY, + }; + + return { + rect, + subRects: layout.items.flatMap(s => s.rects.map(r => r.rect)), + }; + } +} diff --git a/blocksuite/affine/blocks/block-list/src/turbo/list-painter.worker.ts b/blocksuite/affine/blocks/block-list/src/turbo/list-painter.worker.ts new file mode 100644 index 0000000000..c6a3e7c9ec --- /dev/null +++ b/blocksuite/affine/blocks/block-list/src/turbo/list-painter.worker.ts @@ -0,0 +1,114 @@ +import type { + BlockLayout, + BlockLayoutPainter, + TextRect, + WorkerToHostMessage, +} from '@blocksuite/affine-gfx-turbo-renderer'; +import { + BlockLayoutPainterExtension, + getBaseline, +} from '@blocksuite/affine-gfx-turbo-renderer/painter'; + +interface ListItemLayout { + text: string; + rects: TextRect[]; + fontSize: number; + type: 'bulleted' | 'numbered' | 'todo' | 'toggle'; + prefix?: string; + checked?: boolean; + collapsed?: boolean; +} + +export interface ListLayout extends BlockLayout { + type: 'affine:list'; + items: ListItemLayout[]; +} + +const debugListBorder = false; + +function isListLayout(layout: BlockLayout): layout is ListLayout { + return layout.type === 'affine:list'; +} + +class ListLayoutPainter implements BlockLayoutPainter { + private static readonly supportFontFace = + typeof FontFace !== 'undefined' && + typeof self !== 'undefined' && + 'fonts' in self; + + static readonly font = ListLayoutPainter.supportFontFace + ? new FontFace( + 'Inter', + `url(https://fonts.gstatic.com/s/inter/v18/UcCo3FwrK3iLTcviYwYZ8UA3.woff2)` + ) + : null; + + static fontLoaded = !ListLayoutPainter.supportFontFace; + + static { + if (ListLayoutPainter.supportFontFace && ListLayoutPainter.font) { + // @ts-expect-error worker fonts API + self.fonts.add(ListLayoutPainter.font); + + ListLayoutPainter.font + .load() + .then(() => { + ListLayoutPainter.fontLoaded = true; + }) + .catch(error => { + console.error('Failed to load Inter font:', error); + }); + } + } + + paint( + ctx: OffscreenCanvasRenderingContext2D, + layout: BlockLayout, + layoutBaseX: number, + layoutBaseY: number + ): void { + if (!ListLayoutPainter.fontLoaded) { + const message: WorkerToHostMessage = { + type: 'paintError', + error: 'Font not loaded', + blockType: 'affine:list', + }; + self.postMessage(message); + return; + } + + if (!isListLayout(layout)) return; + + const renderedPositions = new Set(); + + layout.items.forEach(item => { + const fontSize = item.fontSize; + const baselineY = getBaseline(fontSize); + + ctx.font = `${fontSize}px Inter`; + ctx.strokeStyle = 'yellow'; + // Render the text content + item.rects.forEach(textRect => { + const x = textRect.rect.x - layoutBaseX; + const y = textRect.rect.y - layoutBaseY; + + const posKey = `${x},${y}`; + // Only render if we haven't rendered at this position before + if (renderedPositions.has(posKey)) return; + + if (debugListBorder) { + ctx.strokeRect(x, y, textRect.rect.w, textRect.rect.h); + } + ctx.fillStyle = 'black'; + ctx.fillText(textRect.text, x, y + baselineY); + + renderedPositions.add(posKey); + }); + }); + } +} + +export const ListLayoutPainterExtension = BlockLayoutPainterExtension( + 'affine:list', + ListLayoutPainter +); diff --git a/blocksuite/affine/blocks/block-list/tsconfig.json b/blocksuite/affine/blocks/block-list/tsconfig.json index 83457418c1..5a8a2133e6 100644 --- a/blocksuite/affine/blocks/block-list/tsconfig.json +++ b/blocksuite/affine/blocks/block-list/tsconfig.json @@ -8,6 +8,7 @@ "include": ["./src"], "references": [ { "path": "../../components" }, + { "path": "../../gfx/turbo-renderer" }, { "path": "../../inlines/preset" }, { "path": "../../model" }, { "path": "../../rich-text" }, diff --git a/blocksuite/affine/blocks/block-paragraph/src/index.ts b/blocksuite/affine/blocks/block-paragraph/src/index.ts index fabfdc09e3..5fa0b7fd43 100644 --- a/blocksuite/affine/blocks/block-paragraph/src/index.ts +++ b/blocksuite/affine/blocks/block-paragraph/src/index.ts @@ -3,5 +3,5 @@ export * from './commands'; export * from './paragraph-block.js'; export * from './paragraph-block-config.js'; export * from './paragraph-spec.js'; -export * from './turbo/paragraph-layout-provider.js'; -export * from './turbo/paragraph-painter.worker.js'; +export * from './turbo/paragraph-layout-handler'; +export * from './turbo/paragraph-painter.worker'; diff --git a/blocksuite/affine/blocks/block-paragraph/src/turbo/paragraph-layout-provider.ts b/blocksuite/affine/blocks/block-paragraph/src/turbo/paragraph-layout-handler.ts similarity index 95% rename from blocksuite/affine/blocks/block-paragraph/src/turbo/paragraph-layout-provider.ts rename to blocksuite/affine/blocks/block-paragraph/src/turbo/paragraph-layout-handler.ts index 3563cca090..a35b3192c3 100644 --- a/blocksuite/affine/blocks/block-paragraph/src/turbo/paragraph-layout-provider.ts +++ b/blocksuite/affine/blocks/block-paragraph/src/turbo/paragraph-layout-handler.ts @@ -15,8 +15,10 @@ export class ParagraphLayoutHandlerExtension extends BlockLayoutHandlerExtension readonly blockType = 'affine:paragraph'; static override setup(di: Container) { - const layoutHandler = new ParagraphLayoutHandlerExtension(); - di.addImpl(BlockLayoutHandlersIdentifier, layoutHandler); + di.addImpl( + BlockLayoutHandlersIdentifier('paragraph'), + ParagraphLayoutHandlerExtension + ); } queryLayout(component: GfxBlockComponent): ParagraphLayout | null { diff --git a/blocksuite/affine/blocks/block-paragraph/src/turbo/paragraph-painter.worker.ts b/blocksuite/affine/blocks/block-paragraph/src/turbo/paragraph-painter.worker.ts index 72c23afef2..4cac0f0e67 100644 --- a/blocksuite/affine/blocks/block-paragraph/src/turbo/paragraph-painter.worker.ts +++ b/blocksuite/affine/blocks/block-paragraph/src/turbo/paragraph-painter.worker.ts @@ -4,7 +4,10 @@ import type { TextRect, WorkerToHostMessage, } from '@blocksuite/affine-gfx-turbo-renderer'; -import { BlockLayoutPainterExtension } from '@blocksuite/affine-gfx-turbo-renderer/painter'; +import { + BlockLayoutPainterExtension, + getBaseline, +} from '@blocksuite/affine-gfx-turbo-renderer/painter'; interface SentenceLayout { text: string; @@ -17,25 +20,8 @@ export interface ParagraphLayout extends BlockLayout { sentences: SentenceLayout[]; } -const meta = { - emSize: 2048, - hHeadAscent: 1984, - hHeadDescent: -494, -}; - const debugSentenceBorder = false; -function getBaseline(fontSize: number) { - const lineHeight = 1.2 * fontSize; - - const A = fontSize * (meta.hHeadAscent / meta.emSize); // ascent - const D = fontSize * (meta.hHeadDescent / meta.emSize); // descent - const AD = A + Math.abs(D); // ascent + descent - const L = lineHeight - AD; // leading - const y = A + L / 2; - return y; -} - function isParagraphLayout(layout: BlockLayout): layout is ParagraphLayout { return layout.type === 'affine:paragraph'; } diff --git a/blocksuite/affine/gfx/turbo-renderer/src/painter/painter.worker.ts b/blocksuite/affine/gfx/turbo-renderer/src/painter/painter.worker.ts index 87eebe3df0..5d1d14f3c2 100644 --- a/blocksuite/affine/gfx/turbo-renderer/src/painter/painter.worker.ts +++ b/blocksuite/affine/gfx/turbo-renderer/src/painter/painter.worker.ts @@ -105,3 +105,20 @@ export class ViewportLayoutPainter { } }; } + +const meta = { + emSize: 2048, + hHeadAscent: 1984, + hHeadDescent: -494, +}; + +export function getBaseline(fontSize: number) { + const lineHeight = 1.2 * fontSize; + + const A = fontSize * (meta.hHeadAscent / meta.emSize); // ascent + const D = fontSize * (meta.hHeadDescent / meta.emSize); // descent + const AD = A + Math.abs(D); // ascent + descent + const L = lineHeight - AD; // leading + const y = A + L / 2; + return y; +} diff --git a/packages/frontend/core/src/blocksuite/extensions/turbo-painter-entry.worker.ts b/packages/frontend/core/src/blocksuite/extensions/turbo-painter-entry.worker.ts index 00369b5900..b9d9e32b8b 100644 --- a/packages/frontend/core/src/blocksuite/extensions/turbo-painter-entry.worker.ts +++ b/packages/frontend/core/src/blocksuite/extensions/turbo-painter-entry.worker.ts @@ -1,4 +1,8 @@ +import { ListLayoutPainterExtension } from '@blocksuite/affine/blocks/list'; import { ParagraphLayoutPainterExtension } from '@blocksuite/affine/blocks/paragraph'; import { ViewportLayoutPainter } from '@blocksuite/affine/gfx/turbo-renderer'; -new ViewportLayoutPainter([ParagraphLayoutPainterExtension]); +new ViewportLayoutPainter([ + ParagraphLayoutPainterExtension, + ListLayoutPainterExtension, +]); diff --git a/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts b/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts index c8cc85805d..7632034873 100644 --- a/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts +++ b/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts @@ -1,3 +1,4 @@ +import { ListLayoutHandlerExtension } from '@blocksuite/affine/blocks/list'; import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph'; import { TurboRendererConfigFactory, @@ -20,6 +21,7 @@ function createPainterWorker() { export function patchTurboRendererExtension() { return [ ParagraphLayoutHandlerExtension, + ListLayoutHandlerExtension, TurboRendererConfigFactory({ options: { zoomThreshold: 1, diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index 867b446b8c..ba26ef2d66 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -250,6 +250,7 @@ export const PackageList = [ name: '@blocksuite/affine-block-list', workspaceDependencies: [ 'blocksuite/affine/components', + 'blocksuite/affine/gfx/turbo-renderer', 'blocksuite/affine/inlines/preset', 'blocksuite/affine/model', 'blocksuite/affine/rich-text', diff --git a/yarn.lock b/yarn.lock index 519f5e8adf..99fddaffe8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2643,6 +2643,7 @@ __metadata: resolution: "@blocksuite/affine-block-list@workspace:blocksuite/affine/blocks/block-list" dependencies: "@blocksuite/affine-components": "workspace:*" + "@blocksuite/affine-gfx-turbo-renderer": "workspace:*" "@blocksuite/affine-inline-preset": "workspace:*" "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-rich-text": "workspace:*"