diff --git a/blocksuite/framework/std/src/gfx/viewport-element.ts b/blocksuite/framework/std/src/gfx/viewport-element.ts index 1ecdac9979..382246da93 100644 --- a/blocksuite/framework/std/src/gfx/viewport-element.ts +++ b/blocksuite/framework/std/src/gfx/viewport-element.ts @@ -1,15 +1,17 @@ import { WithDisposable } from '@blocksuite/global/lit'; +import { batch } from '@preact/signals-core'; import { css, html } from 'lit'; import { property } from 'lit/decorators.js'; -import { PropTypes, requiredProperties } from '../view/decorators/required.js'; import { - type BlockComponent, type EditorHost, + isGfxBlockComponent, ShadowlessElement, -} from '../view/index.js'; -import type { GfxBlockElementModel } from './model/gfx-block-model.js'; -import { Viewport } from './viewport.js'; +} from '../view'; +import { PropTypes, requiredProperties } from '../view/decorators/required'; +import { GfxControllerIdentifier } from './identifiers'; +import type { GfxBlockElementModel } from './model/gfx-block-model'; +import { Viewport } from './viewport'; /** * A wrapper around `requestConnectedFrame` that only calls at most once in one frame @@ -35,24 +37,6 @@ export function requestThrottledConnectedFrame< }) as T; } -function setBlockState(view: BlockComponent | null, state: 'active' | 'idle') { - if (!view) return; - - if (state === 'active') { - view.style.visibility = 'visible'; - view.style.pointerEvents = 'auto'; - view.classList.remove('block-idle'); - view.classList.add('block-active'); - view.dataset.blockState = 'active'; - } else { - view.style.visibility = 'hidden'; - view.style.pointerEvents = 'none'; - view.classList.remove('block-active'); - view.classList.add('block-idle'); - view.dataset.blockState = 'idle'; - } -} - @requiredProperties({ viewport: PropTypes.instanceOf(Viewport), }) @@ -85,21 +69,27 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) { private readonly _hideOutsideBlock = () => { if (!this.host) return; - const { host } = this; + const gfx = this.host.std.get(GfxControllerIdentifier); const modelsInViewport = this.getModelsInViewport(); - modelsInViewport.forEach(model => { - const view = host.std.view.getBlock(model.id); - setBlockState(view, 'active'); + batch(() => { + modelsInViewport.forEach(model => { + const view = gfx.view.get(model); + if (isGfxBlockComponent(view)) { + view.transformState$.value = 'active'; + } - if (this._lastVisibleModels?.has(model)) { - this._lastVisibleModels!.delete(model); - } - }); + if (this._lastVisibleModels?.has(model)) { + this._lastVisibleModels!.delete(model); + } + }); - this._lastVisibleModels?.forEach(model => { - const view = host.std.view.getBlock(model.id); - setBlockState(view, 'idle'); + this._lastVisibleModels?.forEach(model => { + const view = gfx.view.get(model); + if (isGfxBlockComponent(view)) { + view.transformState$.value = 'idle'; + } + }); }); this._lastVisibleModels = modelsInViewport; @@ -194,23 +184,29 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) { setBlocksActive(blockIds: string[]): void { if (!this.host) return; + const gfx = this.host.std.get(GfxControllerIdentifier); - blockIds.forEach(id => { - const view = this.host?.std.view.getBlock(id); - if (view) { - setBlockState(view, 'active'); - } + batch(() => { + blockIds.forEach(id => { + const view = gfx.view.get(id); + if (isGfxBlockComponent(view)) { + view.transformState$.value = 'active'; + } + }); }); } setBlocksIdle(blockIds: string[]): void { if (!this.host) return; + const gfx = this.host.std.get(GfxControllerIdentifier); - blockIds.forEach(id => { - const view = this.host?.std.view.getBlock(id); - if (view) { - setBlockState(view, 'idle'); - } + batch(() => { + blockIds.forEach(id => { + const view = gfx.view.get(id); + if (isGfxBlockComponent(view)) { + view.transformState$.value = 'idle'; + } + }); }); } } diff --git a/blocksuite/framework/std/src/view/element/gfx-block-component.ts b/blocksuite/framework/std/src/view/element/gfx-block-component.ts index 6dd3324504..0b514f7e28 100644 --- a/blocksuite/framework/std/src/view/element/gfx-block-component.ts +++ b/blocksuite/framework/std/src/view/element/gfx-block-component.ts @@ -1,6 +1,6 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { Bound } from '@blocksuite/global/gfx'; -import { computed } from '@preact/signals-core'; +import { computed, effect, signal } from '@preact/signals-core'; import { nothing } from 'lit'; import type { BlockService } from '../../extension/index.js'; @@ -23,7 +23,7 @@ export function isGfxBlockComponent( export const GfxElementSymbol = Symbol('GfxElement'); function updateTransform(element: GfxBlockComponent) { - if (element.dataset.blockState === 'idle') return; + if (element.transformState$.value === 'idle') return; const { viewport } = element.gfx; element.dataset.viewportState = viewport.serializeRecord(); @@ -31,6 +31,20 @@ function updateTransform(element: GfxBlockComponent) { element.style.transform = element.getCSSTransform(); } +function updateBlockVisibility(view: GfxBlockComponent) { + if (view.transformState$.value === 'active') { + view.style.visibility = 'visible'; + view.style.pointerEvents = 'auto'; + view.classList.remove('block-idle'); + view.classList.add('block-active'); + } else { + view.style.visibility = 'hidden'; + view.style.pointerEvents = 'none'; + view.classList.remove('block-active'); + view.classList.add('block-idle'); + } +} + function handleGfxConnection(instance: GfxBlockComponent) { instance.style.position = 'absolute'; @@ -48,7 +62,12 @@ function handleGfxConnection(instance: GfxBlockComponent) { }) ); - updateTransform(instance); + instance.disposables.add( + effect(() => { + updateBlockVisibility(instance); + updateTransform(instance); + }) + ); } export abstract class GfxBlockComponent< @@ -61,6 +80,8 @@ export abstract class GfxBlockComponent< { [GfxElementSymbol] = true; + readonly transformState$ = signal<'idle' | 'active'>('active'); + get gfx() { return this.std.get(GfxControllerIdentifier); } @@ -175,6 +196,8 @@ export function toGfxBlockComponent< return class extends CustomBlock { [GfxElementSymbol] = true; + readonly transformState$ = signal<'idle' | 'active'>('active'); + override selected$ = computed(() => { const selection = this.std.selection.value.find( selection => selection.blockId === this.model?.id diff --git a/tests/blocksuite/e2e/edgeless/presentation.spec.ts b/tests/blocksuite/e2e/edgeless/presentation.spec.ts index 0da3e31784..fea1b3ec01 100644 --- a/tests/blocksuite/e2e/edgeless/presentation.spec.ts +++ b/tests/blocksuite/e2e/edgeless/presentation.spec.ts @@ -8,8 +8,10 @@ import { dragBetweenViewCoords, edgelessCommonSetup, enterPresentationMode, + getSelectedBound, locatorPresentationToolbarButton, resizeElementByHandle, + selectElementInEdgeless, selectNoteInEdgeless, setEdgelessTool, Shape, @@ -271,4 +273,44 @@ test.describe('presentation', () => { const collapseButton = page.getByTestId('edgeless-note-collapse-button'); await expect(collapseButton).not.toBeVisible(); }); + + test('note should be visible when enter presentation mode', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await selectNoteInEdgeless(page, noteId); + const noteBound = await getSelectedBound(page); + await pressEscape(page, 3); + + const frame1 = await createFrame( + page, + [noteBound[0] - 10, noteBound[1] - 10], + [noteBound[0] + noteBound[2] + 10, noteBound[1] + noteBound[3] + 10] + ); + await selectElementInEdgeless(page, [frame1]); + const frame1Bound = await getSelectedBound(page); + await pressEscape(page); + + await createFrame( + page, + [frame1Bound[0] + frame1Bound[2] + 10, frame1Bound[1]], + [frame1Bound[0] + 2 * frame1Bound[2], frame1Bound[1] + frame1Bound[3]] + ); + + await enterPresentationMode(page); + const nextButton = locatorPresentationToolbarButton(page, 'next'); + const prevButton = locatorPresentationToolbarButton(page, 'previous'); + + const note = page.locator('affine-edgeless-note'); + await expect(note).toBeVisible(); + + await nextButton.click(); + await expect(note).toBeHidden(); + + await prevButton.click(); + await expect(note).toBeVisible(); + }); });