feat: add after queued renders promises to render management (#6981)

* feat: add promise to render queue system

* chore: update docs for variables

* chore: add export of render management

* chore: add tests for resolving render management promises

* fix: modify insertion marker manager to use render management promises

* chore: format

* chore: remove callback from afterQueuedRenders

* chore: don't store resolver

* chore: rename afterQueuedRenders -> finishQueuedRenders

* chore: fix tests
This commit is contained in:
Beka Westberg
2023-04-18 17:02:33 -07:00
committed by GitHub
parent a4d0b67985
commit 83e3dcac70
6 changed files with 110 additions and 8 deletions

View File

@@ -1611,10 +1611,13 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
/**
* Triggers a rerender after a delay to allow for batching.
*
* @returns A promise that resolves after the currently queued renders have
* been completed. Used for triggering other behavior that relies on
* updated size/position location for the block.
* @internal
*/
queueRender() {
queueRender(this);
queueRender(): Promise<void> {
return queueRender(this);
}
/**

View File

@@ -121,6 +121,7 @@ import * as uiPosition from './positionable_helpers.js';
import * as Procedures from './procedures.js';
import * as registry from './registry.js';
import {RenderedConnection} from './rendered_connection.js';
import * as renderManagement from './render_management.js';
import * as blockRendering from './renderers/common/block_rendering.js';
import * as constants from './constants.js';
import * as geras from './renderers/geras/geras.js';
@@ -711,6 +712,7 @@ export {Msg, setLocale};
export {Names};
export {Options};
export {RenderedConnection};
export {renderManagement};
export {Scrollbar};
export {ScrollbarPair};
export {ShortcutRegistry};

View File

@@ -12,6 +12,7 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.InsertionMarkerManager');
import {finishQueuedRenders} from './render_management.js';
import * as blockAnimations from './block_animations.js';
import type {BlockSvg} from './block_svg.js';
import * as common from './common.js';
@@ -188,9 +189,9 @@ export class InsertionMarkerManager {
const inferiorConnection = local.isSuperior() ? closest : local;
const rootBlock = this.topBlock.getRootBlock();
// bringToFront is incredibly expensive. Delay by at least a frame.
requestAnimationFrame(() => {
finishQueuedRenders().then(() => {
blockAnimations.connectionUiEffect(inferiorConnection.getSourceBlock());
// bringToFront is incredibly expensive. Delay until the next frame.
setTimeout(() => {
rootBlock.bringToFront();
}, 0);

View File

@@ -8,20 +8,51 @@ import {BlockSvg} from './block_svg.js';
import {Coordinate} from './utils/coordinate.js';
/** The set of all blocks in need of rendering which don't have parents. */
const rootBlocks = new Set<BlockSvg>();
/** The set of all blocks in need of rendering. */
let dirtyBlocks = new WeakSet<BlockSvg>();
let pid = 0;
/**
* The promise which resolves after the current set of renders is completed. Or
* null if there are no queued renders.
*
* Stored so that we can return it from afterQueuedRenders.
*/
let afterRendersPromise: Promise<void>|null = null;
/**
* Registers that the given block and all of its parents need to be rerendered,
* and registers a callback to do so after a delay, to allowf or batching.
*
* @param block The block to rerender.
* @return A promise that resolves after the currently queued renders have been
* completed. Used for triggering other behavior that relies on updated
* size/position location for the block.
* @internal
*/
export function queueRender(block: BlockSvg) {
export function queueRender(block: BlockSvg): Promise<void> {
queueBlock(block);
if (!pid) pid = window.requestAnimationFrame(doRenders);
if (!afterRendersPromise) {
afterRendersPromise = new Promise((resolve) => {
window.requestAnimationFrame(() => {
doRenders();
resolve();
});
});
}
return afterRendersPromise;
}
/**
* @returns A promise that resolves after the currently queued renders have
* been completed.
*/
export function finishQueuedRenders(): Promise<void> {
// If there are no queued renders, return a resolved promise so `then`
// callbacks trigger immediately.
return afterRendersPromise ? afterRendersPromise : Promise.resolve();
}
/**
@@ -62,7 +93,7 @@ function doRenders() {
rootBlocks.clear();
dirtyBlocks = new Set();
pid = 0;
afterRendersPromise = null;
}
/**

View File

@@ -113,6 +113,7 @@
'Blockly.test.procedureMap',
'Blockly.test.procedures',
'Blockly.test.registry',
'Blockly.test.renderManagement',
'Blockly.test.serialization',
'Blockly.test.shortcutRegistry',
'Blockly.test.touch',

View File

@@ -0,0 +1,64 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.declareModuleId('Blockly.test.renderManagement');
import {sharedTestSetup, sharedTestTeardown} from './test_helpers/setup_teardown.js';
suite('Render Management', function() {
setup(function() {
this.clock = sharedTestSetup.call(this).clock;
});
teardown(function() {
sharedTestTeardown.call(this);
});
suite('finish queued renders callback', function() {
function createMockBlock() {
return {
hasRendered: false,
renderEfficiently: function() {this.hasRendered = true;},
// All of the APIs the render management system needs.
getParent: () => null,
getChildren: () => [],
isDisposed: () => false,
getConnections_: () => [],
getRelativeToSurfaceXY: () => ({x: 0, y: 0}),
workspace: {
resizeContents: () => {},
},
};
}
test('the queueRender promise is properly resolved after rendering',
function() {
const block = createMockBlock();
const promise = Blockly.renderManagement.queueRender(block)
.then(() => {
chai.assert.isTrue(
block.hasRendered, 'Expected block to be rendered');
});
this.clock.runAll();
return promise;
});
test(
'the finish queued renders promise is properly resolved after rendering',
function() {
const block = createMockBlock();
Blockly.renderManagement.queueRender(block);
const promise = Blockly.renderManagement.finishQueuedRenders(() => {
chai.assert.isTrue(
block.hasRendered, 'Expected block to be rendered');
});
this.clock.runAll();
return promise;
});
});
});