Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): multitap key-previews 🐵 #10103

Merged
merged 5 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions web/src/engine/osk/src/input/gestures/browser/keytip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,12 @@ export default class KeyTip implements KeyTipInterface {
this.preview = this.previewHost.element;
this.tip.replaceChild(this.preview, oldHost);
previewHost.setCancellationHandler(() => this.show(null, false, null));
previewHost.on('startFade', () => {
this.element.classList.remove('kmw-preview-fade');
// Note: a reflow is needed to reset the transition animation.
this.element.offsetWidth;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply accessing the offsetWidth property retriggers a reflow?

this.element.classList.add('kmw-preview-fade');
});
}
} else { // Hide the key preview
this.element.style.display = 'none';
Expand All @@ -222,6 +228,7 @@ export default class KeyTip implements KeyTipInterface {
const oldPreview = this.preview;
this.preview = document.createElement('div');
this.tip.replaceChild(this.preview, oldPreview);
this.element.classList.remove('kmw-preview-fade');

this.orientation = DEFAULT_TIP_ORIENTATION;
}
Expand Down
20 changes: 15 additions & 5 deletions web/src/engine/osk/src/input/gestures/browser/multitap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GestureHandler } from '../gestureHandler.js';
import { distributionFromDistanceMaps } from '@keymanapp/input-processor';
import Modipress from './modipress.js';
import { keySupportsModipress } from '../specsForLayout.js';
import { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js';

/**
* Represents a potential multitap gesture's implementation within KeymanWeb.
Expand Down Expand Up @@ -35,7 +36,8 @@ export default class Multitap implements GestureHandler {
source: GestureSequence<KeyElement, string>,
vkbd: VisualKeyboard,
e: KeyElement,
contextToken: number
contextToken: number,
previewHost: GesturePreviewHost
) {
this.baseKey = e;
this.baseContextToken = contextToken;
Expand All @@ -54,10 +56,13 @@ export default class Multitap implements GestureHandler {

this.originalLayer = vkbd.layerId;

source.on('complete', () => {
if(source.stageReports.length > 1) {
}
const tapLookahead = (offset) => (this.tapIndex + offset) % this.multitaps.length;

const updatePreview = () => {
previewHost?.setMultitapHint(this.multitaps[tapLookahead(0)].text, this.multitaps[tapLookahead(1)].text);
}

source.on('complete', () => {
this.modipress?.cancel();
this.clear();
});
Expand Down Expand Up @@ -91,8 +96,9 @@ export default class Multitap implements GestureHandler {
}

// For rota-style behavior
this.tapIndex = (this.tapIndex + 1) % this.multitaps.length;
this.tapIndex = tapLookahead(1);
const selection = this.multitaps[this.tapIndex];
updatePreview();

const keyEvent = vkbd.keyEventFromSpec(selection);
keyEvent.baseTranscriptionToken = this.baseContextToken;
Expand Down Expand Up @@ -135,6 +141,10 @@ export default class Multitap implements GestureHandler {
startModipress(source.stageReports[0]);
}

// For this specific instance, we'll go ahead and directly maintain the preview;
// a touch just ended, and all other updates occur on the start of a new touch.
updatePreview();

/* In theory, setting up a specialized recognizer config limited to the base key's surface area
* would be pretty ideal - it'd provide automatic cancellation if anywhere else were touched.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ export class TabletKeyTip implements KeyTipInterface {
this.preview = this.previewHost.element;
this.element.replaceChild(this.preview, oldHost);
previewHost.setCancellationHandler(() => this.show(null, false, null));
previewHost.on('startFade', () => {
this.element.classList.remove('kmw-preview-fade');
// Note: a reflow is needed to reset the transition animation.
this.element.offsetWidth;
this.element.classList.add('kmw-preview-fade');
});
}
} else { // Hide the key preview
this.element.style.display = 'none';
Expand All @@ -105,6 +111,7 @@ export class TabletKeyTip implements KeyTipInterface {
const oldPreview = this.preview;
this.preview = document.createElement('div');
this.element.replaceChild(this.preview, oldPreview);
this.element.classList.remove('kmw-preview-fade');
}

// Save the key preview state
Expand Down
12 changes: 11 additions & 1 deletion web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActiveKey } from "@keymanapp/keyboard-processor";
import { ActiveKey, ActiveKeyBase } from "@keymanapp/keyboard-processor";
import EventEmitter from "eventemitter3";

import { KeyElement } from "../keyElement.js";
Expand All @@ -14,6 +14,7 @@ const FLICK_OVERFLOW_OFFSET = 1.4142;

interface EventMap {
preferredOrientation: (orientation: PhoneKeyTipOrientation) => void;
startFade: () => void;
}

export class GesturePreviewHost extends EventEmitter<EventMap> {
Expand Down Expand Up @@ -137,6 +138,15 @@ export class GesturePreviewHost extends EventEmitter<EventMap> {
this.onCancel = handler;
}

public setMultitapHint(current: string, next: string) {
this.label.textContent = current;
this.hintLabel.textContent = next;

this.emit('startFade');

this.clearFlick();
}

public scrollFlickPreview(x: number, y: number) {
this.clearHint();

Expand Down
47 changes: 29 additions & 18 deletions web/src/engine/osk/src/visualKeyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,17 +428,23 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
}

const endHighlighting = () => {
trackingEntry.previewHost?.cancel();
// If we ever allow concurrent previews, check if it exists and matches
// a VisualKeyboard-tracked entry; if so, clear that too.
if(previewHost) {
this.gesturePreviewHost = null;
trackingEntry.previewHost = null;
}
if(trackingEntry.key) {
this.highlightKey(trackingEntry.key, false);
trackingEntry.key = null;
}
// The base call will occur before our "is this a multitap?" check otherwise.
// That check will unset the field so that it's unaffected by this check.
timedPromise(0).then(() => {
const previewHost = trackingEntry.previewHost;

// If we ever allow concurrent previews, check if it exists and matches
// a VisualKeyboard-tracked entry; if so, clear that too.
if(previewHost) {
previewHost.cancel();
this.gesturePreviewHost = null;
trackingEntry.previewHost = null;
}
if(trackingEntry.key) {
this.highlightKey(trackingEntry.key, false);
trackingEntry.key = null;
}
})
}

// Fix: if flicks enabled, no roaming.
Expand Down Expand Up @@ -510,6 +516,7 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
existingPreviewHost.clearFlick();
}

let trackingEntry: typeof sourceTrackingMap[string];
// Disable roaming-touch highlighting (and current highlighting) for all
// touchpoints included in a gesture, even newly-included ones as they occur.
for(let id of gestureStage.allSourceIds) {
Expand All @@ -522,7 +529,7 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
trackingEntry.source.path.off('step', trackingEntry.roamingHighlightHandler);
}

const trackingEntry = sourceTrackingMap[id];
trackingEntry = sourceTrackingMap[id];

if(trackingEntry) {
clearRoaming(trackingEntry);
Expand Down Expand Up @@ -625,12 +632,16 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
// baseItem is sometimes null during a keyboard-swap... for app/browser touch-based language menus.
// not ideal, but it is what it is; just let it pass by for now.
} else if(baseItem?.key.spec.multitap && (gestureStage.matchedId == 'initial-tap' || gestureStage.matchedId == 'multitap' || gestureStage.matchedId == 'modipress-start')) {
// For now, but worth changing later!
// Idea: if the preview weren't hosted by the key, but instead had a key-lookalike overlay.
// Then it would float above any layer, even after layer swaps.
existingPreviewHost?.cancel();
// Likewise - mere construction is enough.
handlers = [new Multitap(gestureSequence, this, baseItem, keyResult.contextToken)];
// Detach the lifetime of the preview from the current touch.
trackingEntry.previewHost = null;

gestureSequence.on('complete', () => {
existingPreviewHost?.cancel();
this.gesturePreviewHost = null;
})

// Past that, mere construction of the class for delegation is enough.
handlers = [new Multitap(gestureSequence, this, baseItem, keyResult.contextToken, existingPreviewHost)];
} else if(gestureStage.matchedId.indexOf('flick') > -1) {
handlers = [new Flick(
gestureSequence,
Expand Down
10 changes: 10 additions & 0 deletions web/src/resources/osk/kmwosk.css
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,21 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f
z-index: 10002;
}

#kmw-keytip.kmw-preview-fade {
opacity: 0;
transition: 0.15s cubic-bezier(.42,0,.58,1) all 0.35s;
}

.kmw-keypreview {
z-index: 10002;
position: absolute;
}

.kmw-keypreview.kmw-preview-fade {
opacity: 0;
transition: 0.15s cubic-bezier(.42,0,.58,1) all 0.35s;
}

#kmw-gesture-preview {
position: absolute;
display: block;
Expand Down