feat(web): high-level layout op batching

Affects change of keyboards and context resets - particularly when new-context rules trigger a non-default layer
This commit is contained in:
Joshua A. Horton 2024-04-03 11:40:15 +07:00
parent f21afb1476
commit 04df7f76c8
3 changed files with 64 additions and 10 deletions

View file

@ -152,16 +152,32 @@ export default class KeymanEngine<
this.osk.startHide(false);
}
if(this.osk) {
this.osk.setNeedsLayout();
this.osk.activeKeyboard = kbd;
this.osk.present();
}
// Needed to ensure the correct layer is displayed.
// Needs to be after the OSK has loaded for the keyboard in case the default
// layer should be something other than "default" for the current context.
this.core.resetContext(this.contextManager.activeTarget);
const doContextReset = () => {
this.contextManager.resetContext();
}
/*
This pattern is designed to minimize layout reflow during the keyboard-swap process.
The 'default' layer is loaded by default, but some keyboards will start on different
layers depending on the current state of the context.
If possible, we want to only perform layout operations once the correct layer is
set to active.
*/
if(this.osk) {
this.osk.batchLayoutAfter(() => {
this.osk.activeKeyboard = kbd;
// Note: when embedded within the mobile apps, the keyboard will still be visible
// at this time.
doContextReset();
this.osk.present();
});
} else {
doContextReset();
}
});
this.contextManager.on('keyboardasyncload', (metadata) => {
@ -216,7 +232,15 @@ export default class KeymanEngine<
resetContext: (target) => {
// Could reset the target's deadkeys here, but it's really more of a 'core' task.
// So we delegate that to keyboard-processor.
this.core.resetContext(target);
const doReset = () => this.core.resetContext(target);
if(this.osk) {
this.osk.batchLayoutAfter(() => {
doReset();
})
} else {
doReset();
}
},
predictionContext: new PredictionContext(this.core.languageProcessor, this.core.keyboardProcessor),
keyboardCache: this.keyboardRequisitioner.cache

View file

@ -159,6 +159,7 @@ export default abstract class OSKView
private uiStyleSheetManager: StylesheetManager;
private config: Configuration;
private deferLayout: boolean;
private _boxBaseMouseDown: (e: MouseEvent) => boolean;
private _boxBaseTouchStart: (e: TouchEvent) => boolean;
@ -595,8 +596,24 @@ export default abstract class OSKView
this.needsLayout = true;
}
public batchLayoutAfter(closure: () => void) {
try {
this.deferLayout = true;
if(this.vkbd) {
this.vkbd.deferLayout = true;
}
closure();
} finally {
this.deferLayout = false;
if(this.vkbd) {
this.vkbd.deferLayout = false;
}
this.refreshLayout();
}
}
public refreshLayout(pending?: boolean): void {
if(!this.keyboardView) {
if(!this.keyboardView || this.deferLayout) {
return;
}

View file

@ -231,11 +231,20 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
activeGestures: GestureHandler[] = [];
activeModipress: Modipress = null;
private _deferLayout: boolean;
// The keyboard object corresponding to this VisualKeyboard.
public readonly layoutKeyboard: Keyboard;
public readonly layoutKeyboardProperties: KeyboardProperties;
get deferLayout(): boolean {
return this._deferLayout;
}
set deferLayout(value: boolean) {
this._deferLayout = value;
}
get layerId(): string {
return this.layerGroup?.activeLayerId ?? 'default';
}
@ -253,7 +262,7 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
}
}
if(changedLayer) {
if(changedLayer && !this._deferLayout) {
this.updateState();
// We changed the active layer, but not any layout property of the keyboard as a whole.
this.layerGroup.refreshLayout(this.constructLayoutParams());
@ -1206,6 +1215,10 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
* when needed.
*/
refreshLayout() {
if(this._deferLayout) {
return;
}
/*
Phase 1: calculations possible at the start without triggering _any_ additional layout reflow.
(A single, initial reflow may happen depending on DOM manipulations before this method...,