Skip to content

Commit

Permalink
feat(ui): support switching render unit (#4580)
Browse files Browse the repository at this point in the history
Co-authored-by: GitHub Actions <[email protected]>
  • Loading branch information
wzhudev and actions-user authored Feb 5, 2025
1 parent ce0b939 commit 45a1c14
Show file tree
Hide file tree
Showing 19 changed files with 346 additions and 78 deletions.
48 changes: 48 additions & 0 deletions e2e/visual-comparison/sheets/sheets-switching-render-unit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { chromium, expect, test } from '@playwright/test';
import { generateSnapshotName } from '../const';

const isCI = !!process.env.CI;

test('ensure switching render unit successful with no errors', async () => {
const browser = await chromium.launch({
headless: isCI, // Set to false to see the browser window
});
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 2, // Set your desired DPR
});
const page = await context.newPage();

await page.goto('http://localhost:3000/sheets/');
await page.waitForTimeout(2000);

await page.evaluate(() => window.E2EControllerAPI.loadDemoSheet());
await page.waitForTimeout(1000);

await page.evaluate(() => window.E2EControllerAPI.loadDefaultSheet());
const filename = generateSnapshotName('switching-render-unit');
const firstScreenshot = await page.screenshot({
mask: [
page.locator('.univer-headerbar'),
page.locator('.univer-defined-name'),
],
fullPage: true,
});
expect(firstScreenshot).toMatchSnapshot(filename, { maxDiffPixels: 50 });
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ test('diff default sheet toolbar', async () => {
],
fullPage: true,
});
await expect(screenshot).toMatchSnapshot(filename, { maxDiffPixels: 100 });
expect(screenshot).toMatchSnapshot(filename, { maxDiffPixels: 100 });
});

test('diff default sheet content', async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion mockdata/src/sheets/demo/default-workbook-data-demo4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const richTextDemo1: IDocumentData = {
};

export const DEFAULT_WORKBOOK_DATA_DEMO4: IWorkbookData = {
id: 'workbook-01',
id: 'workbook-04',
locale: LocaleType.ZH_CN,
name: 'universheet',
sheetOrder: ['sheet-0004'],
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ export { ConfigService } from './services/config/config.service';
export * from './services/context/context';
export { ContextService, IContextService } from './services/context/context.service';
export { ErrorService, type IError } from './services/error/error.service';
export { IUniverInstanceService } from './services/instance/instance.service';
export { UniverInstanceService } from './services/instance/instance.service';
export { type ICreateUnitOptions, IUniverInstanceService, UniverInstanceService } from './services/instance/instance.service';
export { LifecycleStages } from './services/lifecycle/lifecycle';
export { LifecycleService } from './services/lifecycle/lifecycle.service';
export { ILocalStorageService } from './services/local-storage/local-storage.service';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ export function createCommandTestBed(docData?: IDocumentData, dependencies?: Dep
mainComponent: null as any,
components: null as any,
isMainScene: true,
activated$: new BehaviorSubject(true),
activate: () => {},
deactivate: () => {},
}, univerInstanceService);

injector.add([DocSkeletonManagerService, { useValue: fakeDocSkeletonManager as unknown as DocSkeletonManagerService }]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,16 @@ export class DocRenderController extends RxDisposable implements IRenderModule {

this._addComponent();

engine.runRenderLoop(() => {
scene.render();
});
const frameFn = () => scene.render();
this.disposeWithMe(this._context.activated$.subscribe((activated) => {
if (activated) {
// TODO: we should attach the context object to the RenderContext object on scene.canvas.
engine.runRenderLoop(frameFn);
} else {
// Stop the render loop when the render unit is deactivated.
engine.stopRenderLoop(frameFn);
}
}));

// Attach scroll event after main viewport created.
this._docSelectionRenderService.__attachScrollEvent();
Expand Down
58 changes: 47 additions & 11 deletions packages/engine-render/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@
* limitations under the License.
*/

import type { Nullable } from '@univerjs/core';
import type { IDisposable, Nullable } from '@univerjs/core';

import type { CURSOR_TYPE } from './basics/const';
import type { IEvent, IKeyboardEvent, IPointerEvent } from './basics/i-events';
import type { ITimeMetric, ITransformChangeState } from './basics/interfaces';
import type { IBasicFrameInfo } from './basics/performance-monitor';
import type { Scene } from './scene';
import { Disposable, EventSubject, toDisposable, Tools } from '@univerjs/core';
import { Observable, shareReplay, Subject } from 'rxjs';
import { type CURSOR_TYPE, RENDER_CLASS_TYPE } from './basics/const';
import { RENDER_CLASS_TYPE } from './basics/const';
import { DeviceType, PointerInput } from './basics/i-events';
import { TRANSFORM_CHANGE_OBSERVABLE_TYPE } from './basics/interfaces';
import { PerformanceMonitor } from './basics/performance-monitor';
Expand Down Expand Up @@ -298,20 +299,50 @@ export class Engine extends Disposable {
return this.getCanvas().getPixelRatio();
}

setContainer(elem: HTMLElement, resize = true) {
if (this._container === elem) {
private _resizeListenerDisposable: IDisposable | undefined;

/**
* Mount the canvas to the element so it would be rendered on UI.
* @param {HTMLElement} element - The element the canvas will mount on.
* @param {true} [resize] If should perform resize when mounted and observe resize event.
*/
mount(element: HTMLElement, resize = true): void {
this.setContainer(element, resize);
}

/**
* Unmount the canvas without disposing it so it can be mounted again.
*/
unmount(): void {
this._clearResizeListener();

if (!this._container) {
throw new Error('[Engine]: cannot unmount when container is not set!');
}

this._container.removeChild(this.getCanvasElement());
this._container = null;
}

/**
* Mount the canvas to the element so it would be rendered on UI.
* @deprecated Please use `mount` instead.
* @param {HTMLElement} element - The element the canvas will mount on.
* @param {true} [resize] If should perform resize when mounted and observe resize event.
*/
setContainer(element: HTMLElement, resize = true) {
if (this._container === element) {
return;
}

this._container = elem;
this._container = element;
this._container.appendChild(this.getCanvasElement());

this._clearResizeListener();

if (resize) {
this.resize();

this._resizeObserver?.unobserve(this._container as HTMLElement);
this._resizeObserver = null;

let timer: number | undefined;
this._resizeObserver = new ResizeObserver(() => {
if (!timer) {
Expand All @@ -323,13 +354,18 @@ export class Engine extends Disposable {
});
this._resizeObserver.observe(this._container);

this.disposeWithMe(() => {
this._resizeObserver?.unobserve(this._container as HTMLElement);
this._resizeListenerDisposable = toDisposable(() => {
this._resizeObserver!.unobserve(this._container as HTMLElement);
if (timer !== undefined) window.cancelIdleCallback(timer);
});
}
}

private _clearResizeListener(): void {
this._resizeListenerDisposable?.dispose();
this._resizeListenerDisposable = undefined;
}

resize() {
if (!this._container) {
return;
Expand Down Expand Up @@ -408,7 +444,7 @@ export class Engine extends Disposable {
this._beginFrame$.complete();
this._endFrame$.complete();

this._resizeObserver?.disconnect();
this._clearResizeListener();
this._container = null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,18 @@ import type { DocComponent } from '../components/docs/doc-component';

import type { SheetComponent } from '../components/sheets/sheet-component';
import type { Slide } from '../components/slides/slide';
import type { IRender } from './render-unit';
import { createIdentifier, Disposable, Inject, Injector, IUniverInstanceService, remove, toDisposable, UniverInstanceType } from '@univerjs/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { Engine } from '../engine';
import { Scene } from '../scene';
import { type IRender, RenderUnit } from './render-unit';
import { RenderUnit } from './render-unit';

export type RenderComponentType = SheetComponent | DocComponent | Slide | BaseObject;

export interface IRenderManagerService extends IDisposable {
/** @deprecated */
currentRender$: Observable<Nullable<string>>;
getCurrent(): Nullable<IRender>;

addRender(unitId: string, renderer: IRender): void;

Expand Down Expand Up @@ -71,8 +72,6 @@ export interface IRenderManagerService extends IDisposable {
created$: Observable<IRender>;
disposed$: Observable<string>;

/** @deprecated There will be multi units to render at the same time, so there is no *current*. */
getCurrent(): Nullable<IRender>;
/** @deprecated There will be multi units to render at the same time, so there is no *first*. */
getFirst(): Nullable<IRender>;

Expand Down Expand Up @@ -332,7 +331,6 @@ export class RenderManagerService extends Disposable implements IRenderManagerSe

setCurrent(unitId: string): void {
this._currentUnitId = unitId;

this._currentRender$.next(unitId);
}

Expand Down
45 changes: 45 additions & 0 deletions packages/engine-render/src/render-manager/render-unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
*/

import type { Dependency, DependencyIdentifier, IDisposable, Nullable, UnitModel, UnitType, UniverInstanceType } from '@univerjs/core';
import type { Observable } from 'rxjs';
import type { Engine } from '../engine';
import type { Scene } from '../scene';
import type { RenderComponentType } from './render-manager.service';
import { Disposable, Inject, Injector, isClassDependencyItem } from '@univerjs/core';
import { BehaviorSubject, distinctUntilChanged } from 'rxjs';

/**
* Public interface of a {@link RenderUnit}.
Expand All @@ -35,8 +37,24 @@ export interface IRender {
isMainScene: boolean;
isThumbNail?: boolean;

/**
* Whether the render unit is activated. It should emit value when subscribed immediately.
* When created, the render unit is activated by default.
*/
activated$: Observable<boolean>;

with<T>(dependency: DependencyIdentifier<T>): T;
getRenderContext?(): IRenderContext;
/**
* Deactivate the render unit, means the render unit would be freezed and not updated,
* even removed from the webpage. However, the render unit is still in the memory and
* could be activated again.
*/
deactivate(): void;
/**
* Activate the render unit, means the render unit would be updated and rendered.
*/
activate(): void;
}

/**
Expand All @@ -60,6 +78,9 @@ export interface IRenderContext<T extends UnitModel = UnitModel> extends Omit<IR
export class RenderUnit extends Disposable implements IRender {
readonly isRenderUnit: boolean = true;

private readonly _activated$ = new BehaviorSubject<boolean>(true);
readonly activated$ = this._activated$.pipe(distinctUntilChanged());

get unitId(): string { return this._renderContext.unitId; }
get type(): UnitType { return this._renderContext.type; }

Expand Down Expand Up @@ -94,12 +115,28 @@ export class RenderUnit extends Disposable implements IRender {
isMainScene: init.isMainScene,
engine: init.engine,
scene: init.scene,
activated$: this.activated$,
activate: () => this._activated$.next(true),
deactivate: () => this._activated$.next(false),
};
}

override dispose(): void {
this._injector.dispose();

super.dispose();

this._activated$.next(false);
this._activated$.complete();

// Avoid memory leak. Basically it is because RenderUnit itself is leaking.
// We use this as a temporary solution to make CI pass.
// @ts-ignore
this._renderContext.activated$ = null;
// @ts-ignore
this._renderContext.activate = null;
// @ts-ignore
this._renderContext.deactivate = null;
}

/**
Expand Down Expand Up @@ -145,4 +182,12 @@ export class RenderUnit extends Disposable implements IRender {
getRenderContext(): IRenderContext {
return this._renderContext;
}

activate(): void {
this._renderContext.activate();
}

deactivate(): void {
this._renderContext.deactivate();
}
}
Loading

0 comments on commit 45a1c14

Please sign in to comment.