Skip to content

Commit

Permalink
Merge pull request #11129 from keymanapp/change/web/no-nearest-key-re…
Browse files Browse the repository at this point in the history
…flow
  • Loading branch information
jahorton authored Apr 3, 2024
2 parents a067401 + f5bb539 commit 333d465
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 67 deletions.
140 changes: 73 additions & 67 deletions web/src/engine/osk/src/keyboard-layout/oskLayerGroup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActiveLayer, type DeviceSpec, Keyboard, LayoutLayer, ActiveLayout } from '@keymanapp/keyboard-processor';
import { ActiveLayer, type DeviceSpec, Keyboard, LayoutLayer, ActiveLayout, ButtonClasses } from '@keymanapp/keyboard-processor';
import { ManagedPromise } from '@keymanapp/web-utils';

import { InputSample } from '@keymanapp/gesture-recognizer';
Expand All @@ -7,12 +7,19 @@ import { KeyElement } from '../keyElement.js';
import OSKLayer from './oskLayer.js';
import VisualKeyboard from '../visualKeyboard.js';
import OSKRow from './oskRow.js';
import OSKBaseKey from './oskBaseKey.js';

const NEAREST_KEY_TOUCH_MARGIN_PERCENT = 0.06;

export default class OSKLayerGroup {
public readonly element: HTMLDivElement;
public readonly layers: {[layerID: string]: OSKLayer} = {};
public readonly spec: ActiveLayout;

// Exist as local copies of the VisualKeyboard values, updated via refreshLayout.
private computedWidth: number;
private computedHeight: number;

private _activeLayerId: string = 'default';

public constructor(vkbd: VisualKeyboard, keyboard: Keyboard, formFactor: DeviceSpec.FormFactor) {
Expand Down Expand Up @@ -107,8 +114,6 @@ export default class OSKLayerGroup {
throw new Error(`Layer id ${layerId} could not be found`);
}

this.blinkLayer(layer);

return this.nearestKey(coord, layer);
}

Expand Down Expand Up @@ -165,86 +170,87 @@ export default class OSKLayerGroup {
}

private nearestKey(coord: Omit<InputSample<KeyElement>, 'item'>, layer: OSKLayer): KeyElement {
const baseRect = this.element.getBoundingClientRect();

let row: OSKRow = null;
let bestMatchDistance = Number.MAX_VALUE;
// If there are no rows, there are no keys; return instantly.
if(layer.rows.length == 0) {
return null;
}

// Find the row that the touch-coordinate lies within.
for(const r of layer.rows) {
const rowRect = r.element.getBoundingClientRect();
if(rowRect.top <= coord.clientY && coord.clientY < rowRect.bottom) {
row = r;
break;
} else {
const distance = rowRect.top > coord.clientY ? rowRect.top - coord.clientY : coord.clientY - rowRect.bottom;
// Our pre-processed layout info maps whatever shape the keyboard is in into a unit square.
// So, we map our coord to find its location within that square.
const proportionalCoords = {
x: coord.targetX / this.computedWidth,
y: coord.targetY / this.computedHeight
};

if(distance < bestMatchDistance) {
bestMatchDistance = distance;
row = r;
}
}
// If our computed width and/or height are 0, it's best to abort; key distance
// calculations are not viable.
if(!isFinite(proportionalCoords.x) || !isFinite(proportionalCoords.y)) {
return null;
}

// Assertion: row no longer `null`.
// Step 1: find the nearest row.
// Rows aren't variable-height - this value is "one size fits all."

// Warning: am not 100% sure that what follows is actually fully correct.
/*
If 4 rows, y = .2 x 4 = .8 - still within the row with index 0 (spanning from 0 to .25)
y = .6 x 4 = 2.4 - within row with index 2 (third row, spanning .5 to .75)
// Find minimum distance from any key
let closestKeyIndex = 0;
let dx: number;
let dxMax = 24;
let dxMin = 100000;
Assumes there is no fine-tuning of the row ranges to be done - each takes a perfect
fraction of the overall layer height without any padding above or below.
*/
const rowIndex = Math.floor(proportionalCoords.y * layer.rows.length);
const row = layer.rows[rowIndex];

const x = coord.clientX;

for (let k = 0; k < row.keys.length; k++) {
// Second-biggest, though documentation suggests this is probably right.
const keySquare = row.keys[k].square as HTMLElement; // gets the .kmw-key-square containing a key
const squareRect = keySquare.getBoundingClientRect();
// Assertion: row no longer `null`.
// (We already prevented the no-rows available scenario, anyway.)

// Find the actual key element.
let childNode = keySquare.firstChild ? keySquare.firstChild as HTMLElement : keySquare;
// Step 2: Find minimum distance from any key
// - If the coord is within a key's square, go ahead and return it.
let closestKey: OSKBaseKey = null;
// Is percentage-based!
let minDistance = Number.MAX_VALUE;

if (childNode.className !== undefined
&& (childNode.className.indexOf('key-hidden') >= 0
|| childNode.className.indexOf('key-blank') >= 0)) {
for (let key of row.keys) {
const keySpec = key.spec;
if(keySpec.sp == ButtonClasses.blank || keySpec.sp == ButtonClasses.spacer) {
continue;
}
const x1 = squareRect.left;
const x2 = squareRect.right;
if (x >= x1 && x <= x2) {
// Within the key square
return <KeyElement>childNode;
}
dx = x1 - x;
if (dx >= 0 && dx < dxMin) {
// To right of key
closestKeyIndex = k; dxMin = dx;
}
dx = x - x2;
if (dx >= 0 && dx < dxMin) {
// To left of key
closestKeyIndex = k; dxMin = dx;
}
}

if (dxMin < 100000) {
const t = <HTMLElement>row.keys[closestKeyIndex].square;
const squareRect = t.getBoundingClientRect();
// Max distance from the key's center to consider, horizontally.
const keyRadius = keySpec.proportionalWidth / 2;
const distanceFromCenter = Math.abs(proportionalCoords.x - keySpec.proportionalX);

const x1 = squareRect.left;
const x2 = squareRect.right;

// Limit extended touch area to the larger of 0.6 of key width and 24 px
if (squareRect.width > 40) {
dxMax = 0.6 * squareRect.width;
// Find the actual key element.
if(distanceFromCenter - keyRadius <= 0) {
// As noted above: if we land within a key's square, match instantly!
return key.btn;
} else {
const distance = distanceFromCenter - keyRadius;
if(distance < minDistance) {
minDistance = distance;
closestKey = key;
}
}
}

if (((x1 - x) >= 0 && (x1 - x) < dxMax) || ((x - x2) >= 0 && (x - x2) < dxMax)) {
return <KeyElement>t.firstChild;
}
/*
Step 3: If the input coordinate wasn't within any valid key's "square",
determine if the nearest valid key is acceptable - if it's within 60% of
a standard key's width from the touch location.
If the condition is not met, there are no valid keys within this row.
*/
if (minDistance /* %age-based! */ <= NEAREST_KEY_TOUCH_MARGIN_PERCENT) {
return closestKey.btn;
}

// Step 4: no matches => return null. The caller should be able to handle such cases,
// anyway.
return null;
}

public refreshLayout(computedWidth: number, computedHeight: number) {
this.computedWidth = computedWidth;
this.computedHeight = computedHeight;
}
}
2 changes: 2 additions & 0 deletions web/src/engine/osk/src/keyboard-layout/oskRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default class OSKRow {
public readonly element: HTMLDivElement;
public readonly keys: OSKBaseKey[];
public readonly heightFraction: number;
public readonly spec: ActiveRow;

public constructor(vkbd: VisualKeyboard,
layerSpec: ActiveLayer,
Expand All @@ -23,6 +24,7 @@ export default class OSKRow {

// Apply defaults, setting the width and other undefined properties for each key
const keys=rowSpec.key;
this.spec = rowSpec;
this.keys = [];

// Calculate actual key widths by multiplying by the OSK's width and rounding appropriately,
Expand Down
4 changes: 4 additions & 0 deletions web/src/engine/osk/src/visualKeyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,10 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
return;
}

// Set layer-group copies of the computed-size values; they are used by nearest-key
// detection.
this.layerGroup.refreshLayout(this._computedWidth, this._computedHeight);

// Step 3: recalculate gesture parameter values
// Skip for doc-keyboards, since they don't do gestures.
if(!this.isStatic) {
Expand Down

0 comments on commit 333d465

Please sign in to comment.