Skip to content

Commit

Permalink
Merge pull request #11140 from keymanapp/change/web/anti-layout-reflo…
Browse files Browse the repository at this point in the history
…w-thrashing

change(web):  OSK optimization, improved responsiveness 🪠
  • Loading branch information
jahorton authored Apr 12, 2024
2 parents 68f6a24 + 8261f7f commit ab0d225
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 22 deletions.
2 changes: 0 additions & 2 deletions web/src/engine/main/src/contextManagerBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,6 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
// (!wasNull || !!keyboard) - blocks events for `null` -> `null` transitions.
// (keyman/keymanweb.com#96)
if(this.currentKeyboardSrcTarget() == originalKeyboardTarget && (!wasNull || !!keyboard)) {
// Perform standard context-reset ops, including the processing of new-context events.
this.resetContext();
// Will trigger KeymanEngine handler that passes keyboard to the OSK, displays it.
this.emit('keyboardchange', this.activeKeyboard);
}
Expand Down
70 changes: 52 additions & 18 deletions web/src/engine/main/src/keymanEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type Keyboard, KeyboardKeymanGlobal, ProcessorInitOptions } from "@keym
import { DOMKeyboardLoader as KeyboardLoader } from "@keymanapp/keyboard-processor/dom-keyboard-loader";
import { InputProcessor, PredictionContext } from "@keymanapp/input-processor";
import { OSKView } from "keyman/engine/osk";
import { KeyboardRequisitioner, ModelCache, ModelSpec, toUnprefixedKeyboardId as unprefixed } from "keyman/engine/package-cache";
import { KeyboardRequisitioner, KeyboardStub, ModelCache, ModelSpec, toUnprefixedKeyboardId as unprefixed } from "keyman/engine/package-cache";

import { EngineConfiguration, InitOptionSpec } from "./engineConfiguration.js";
import KeyboardInterface from "./keyboardInterface.js";
Expand Down Expand Up @@ -137,31 +137,57 @@ export default class KeymanEngine<
});

this.contextManager.on('keyboardchange', (kbd) => {
this.refreshModel();
this.core.activeKeyboard = kbd?.keyboard;

this.legacyAPIEvents.callEvent('keyboardchange', {
internalName: kbd?.metadata.id ?? '',
languageCode: kbd?.metadata.langId ?? ''
});

// Hide OSK and do not update keyboard list if using internal keyboard (desktops).
// Condition will not be met for touch form-factors; they force selection of a
// default keyboard.
if(!kbd) {
this.osk.startHide(false);
}

if(this.osk) {
this.osk.setNeedsLayout();
this.osk.activeKeyboard = kbd;
this.osk.present();
const earlyBatchClosure = () => {
this.refreshModel();
// Triggers context resets that can trigger layout stuff.
// It's not the final such context-reset, though.
this.core.activeKeyboard = kbd?.keyboard;

this.legacyAPIEvents.callEvent('keyboardchange', {
internalName: kbd?.metadata.id ?? '',
languageCode: kbd?.metadata.langId ?? ''
});
}

/*
Needed to ensure the correct layer is displayed AND that deadkeys from
the old keyboard have been wiped.
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.
*/
const doContextReset = () => {
this.contextManager.resetContext();
}

// 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);
/*
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(() => {
earlyBatchClosure();
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 {
earlyBatchClosure();
doContextReset();
}
});

this.contextManager.on('keyboardasyncload', (metadata) => {
Expand Down Expand Up @@ -216,7 +242,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
Expand Down
28 changes: 27 additions & 1 deletion web/src/engine/osk/src/views/oskView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -595,8 +596,33 @@ export default abstract class OSKView
this.needsLayout = true;
}

public batchLayoutAfter(closure: () => void) {
/*
Is there already an ongoing batch? If so, just run the closure and don't
adjust the tracking variables. The outermost call will finalize layout.
*/
if(this.deferLayout) {
closure();
return;
}

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;
}

Expand Down
15 changes: 14 additions & 1 deletion web/src/engine/osk/src/visualKeyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand All @@ -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());
Expand Down Expand Up @@ -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...,
Expand Down

0 comments on commit ab0d225

Please sign in to comment.