mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 01:00:13 +02:00
Steam's UpdateTouchState counts the event's `touches` per key: that count is the pressed highlight (rgLayoutTouchCount -> Touched) and where the long press (Backspace repeat, accents) starts. The touch-start event left the new touch out of `touches`, like a touchend; now it is in, as in a DOM touchstart (tracker.js touchEvent, with a test against a model of Steam's handlers).
116 lines
5.9 KiB
JavaScript
116 lines
5.9 KiB
JavaScript
// Touch typing (vr-keyboard-touch.nix; patch name "vr-keyboard-touch"),
|
|
// injected into Steam's SharedJSContext (8080). mkPatch convention plus two
|
|
// more arguments: tracker.js and vr-keyboard-controllers' hub.js.
|
|
//
|
|
// The controller frames (hub.js; the tips relative to the keyboard) go
|
|
// through one tracker.js per hand; its events press keys with the keyboard's
|
|
// own touch handlers, like a laser press (the laser arrives as touch
|
|
// events): HandleTouchStart at "down" (key highlighted, sound, Steam's long
|
|
// press: repeating Backspace, the accent popup), HandleTouchEnd at "up"
|
|
// (Steam types the key, unless its long press already did). The key is the
|
|
// [data-key] element at the point (skipping MouseHoverBlockerHack, like
|
|
// Steam's hit test); no key there: nothing. Each hand is its own touch, so
|
|
// both hands can overlap like two fingers (Steam's multi-touch: Shift held
|
|
// by one hand). No DOM events: the swipe patch's and the extra keys'
|
|
// listeners don't see these touches; the swipe patch's text model does (it
|
|
// hooks the methods the keys go through).
|
|
// "cancel" (no pose), frames stopping for STALE_MS, the keyboard moving or
|
|
// closing end a touch without typing. Debugging: __sfuiTouchTypeLog.
|
|
((find, sigs, opts, hooks, TR, HUB) => {
|
|
const VERSION = 3;
|
|
const G = window;
|
|
const STALE_MS = 1000; // Steam's UI thread can stall frames for ~0.3 s
|
|
const SNAP = 3; // SteamVR overlay haptic effect (1 ButtonEnter, 3 Snap)
|
|
const HANDS = ['left', 'right'];
|
|
const ID = { left: 0x5f10, right: 0x5f11 }; // touch identifiers
|
|
let status;
|
|
try {
|
|
const mods = find.resolveAll(find.getWebpackRequire('webpackChunksteamui'), sigs);
|
|
status = mods.vrStatus.exports.holder;
|
|
} catch (e) {
|
|
return `Steam internals changed, no touch typing: ${e.message}`;
|
|
}
|
|
const stamp = `${VERSION}:${TR.VERSION}:${HUB.version} ${JSON.stringify(opts)}`;
|
|
if (G.__sfuiTouchType?.stamp === stamp) return 'unchanged';
|
|
try { G.__sfuiTouchType?.dispose?.(); } catch { /* gone */ }
|
|
|
|
const log = (...a) => {
|
|
const l = (G.__sfuiTouchTypeLog ??= []);
|
|
l.push([Math.round(performance.now()), ...a]);
|
|
if (l.length > 200) l.splice(0, l.length - 200);
|
|
};
|
|
const kbInst = (el) => find.findFiberUp(el, (f) => typeof f.stateNode?.HandleTouchStart === 'function' &&
|
|
typeof f.stateNode?.HandleTouchEnd === 'function' && f.stateNode.m_mapTouched instanceof Set, 200)?.stateNode;
|
|
const touches = new Map(); // hand -> { t, inst, doc }
|
|
|
|
function keyboardWindow() {
|
|
const p = [...(G.g_PopupManager?.GetPopups?.() || [])].find((x) => x.window?.document.querySelector('[data-key]'));
|
|
return p?.window ?? null;
|
|
}
|
|
// Steam's handlers read target, changedTouches, touches (tracker.js
|
|
// touchEvent) and the touches' target / clientX / clientY.
|
|
const others = (t, inst) => [...touches.values()].filter((e) => e.inst === inst && e.t !== t).map((e) => e.t);
|
|
|
|
function end(hand, { cancel = false, u, v } = {}) {
|
|
const e = touches.get(hand);
|
|
if (!e) return;
|
|
touches.delete(hand);
|
|
// Where it left, if on the page (an accent picked in Steam's popup);
|
|
// Steam types nothing for a release off the page.
|
|
const win = e.doc.defaultView, x = u * win?.innerWidth, y = v * win?.innerWidth;
|
|
if (!cancel && x >= 0 && y >= 0 && x < win.innerWidth && y < win.innerHeight) Object.assign(e.t, { clientX: x, clientY: y, pageX: x, pageY: y });
|
|
if (cancel) e.inst.m_mapTouched.delete(e.t.target); // Steam types only keys still in m_mapTouched
|
|
try { e.inst.HandleTouchEnd(TR.touchEvent(e.t, others(e.t, e.inst), false)); } catch (err) { log('end-error', String(err)); }
|
|
log(hand, cancel ? 'cancel' : 'up', e.t.target.getAttribute('data-key'));
|
|
}
|
|
|
|
function down(hand, u, v) {
|
|
end(hand, { cancel: true });
|
|
const win = keyboardWindow();
|
|
if (!win) return;
|
|
const doc = win.document, x = u * win.innerWidth, y = v * win.innerWidth;
|
|
const el = doc.elementsFromPoint(x, y).find((e) => e.id !== 'MouseHoverBlockerHack');
|
|
const keyEl = el?.closest?.('[data-key]');
|
|
const inst = keyEl && kbInst(keyEl);
|
|
if (!inst) { log(hand, 'miss', Math.round(x), Math.round(y)); return; }
|
|
const t = { identifier: ID[hand], target: keyEl, clientX: x, clientY: y, pageX: x, pageY: y };
|
|
touches.set(hand, { t, inst, doc });
|
|
try { inst.HandleTouchStart(TR.touchEvent(t, others(t, inst), true)); } catch (err) { log('start-error', String(err)); end(hand, { cancel: true }); return; }
|
|
if (opts.haptics) win.SteamClient?.OpenVR?.TriggerOverlayHapticEffect?.(SNAP, 0);
|
|
log(hand, 'down', keyEl.getAttribute('data-key'), Math.round(x), Math.round(y));
|
|
}
|
|
|
|
const trackers = Object.fromEntries(HANDS.map((h) => [h, TR.createTracker({ depth: opts.depth })]));
|
|
let lastT = 0, lastAt = -Infinity;
|
|
function step(hand, sample, t) {
|
|
const ev = trackers[hand].update(sample, t);
|
|
if (ev?.phase === 'down') down(hand, ev.u, ev.v);
|
|
else if (ev?.phase === 'up') end(hand, { u: ev.u, v: ev.v });
|
|
else if (ev?.phase === 'cancel') end(hand, { cancel: true });
|
|
}
|
|
const unsubscribe = HUB.subscribe((f) => {
|
|
const now = performance.now();
|
|
const t = f?.t ?? lastT + (now - lastAt);
|
|
lastT = t; lastAt = now;
|
|
const usable = !!(f?.keyboard && !f.moving && status.VRKeyboardStatus?.bIsOpen);
|
|
for (const h of HANDS) step(h, usable ? f.hands?.[h]?.tip ?? null : null, t);
|
|
});
|
|
// No frames (relay or SteamVR gone) or the keyboard closed: end the touches.
|
|
const watchdog = setInterval(() => {
|
|
const now = performance.now();
|
|
if (now - lastAt > STALE_MS || !status.VRKeyboardStatus?.bIsOpen) {
|
|
for (const h of HANDS) if (trackers[h].pressed || touches.has(h)) { step(h, null, lastT + (now - lastAt)); end(h, { cancel: true }); }
|
|
}
|
|
}, 250);
|
|
|
|
G.__sfuiTouchType = {
|
|
stamp,
|
|
dispose() {
|
|
unsubscribe(); clearInterval(watchdog);
|
|
for (const h of HANDS) end(h, { cancel: true });
|
|
},
|
|
};
|
|
log('attached', VERSION);
|
|
return 'patched';
|
|
})
|