window-curvature, frame-controls: a curvature drag cancels the long press after the ring too

When the ring shows, frame-controls no longer takes the press over via
cancelPress(); it raises the press's drag threshold to 3x via the new
window.__sfuiWindowCurvature.scalePressDragThreshold(factor) (measured from
the press start). Laser drift keeps the long press, a deliberate drag starts
the curvature drag and cancels it. Completion still calls cancelPress().
This commit is contained in:
Pierre Kisters committed 2026-09-28 05:49:45 +02:00
1 parent 885a97aa59
commit c4401fa99f
3 files changed
+52 -26

No files matched your search

+13 -7
View File
@@ -706,11 +706,15 @@ window until SteamVR restarts. Debugging: `window.__sfuiWindowCurvature.dump()`
every element the patch drives has class `sfui-curv-ctl`; when a press
becomes a drag, a bubbling `CustomEvent` `sfui-curv-dragstart` (detail
`{ frameID, where: 'menu' | 'bar' }`) is dispatched on it, and
`sfui-curv-dragend` when it ends; `window.__sfuiWindowCurvature.cancelPress()`
ends a press without its click. A patch with its own gesture lets
`mousemove` through while undecided, drops its gesture on
`sfui-curv-dragstart`, and calls `cancelPress()` when it takes the press
over.
`sfui-curv-dragend` when it ends;
`window.__sfuiWindowCurvature.scalePressDragThreshold(factor)` sets the
current press's drag threshold to `factor` × `dragThresholdPixels` (from the
press start; returns whether it applied, i.e. a press that is not yet a
drag); `window.__sfuiWindowCurvature.cancelPress()` ends a press without its
click. A patch with its own gesture lets `mousemove` through while
undecided, drops its gesture on `sfui-curv-dragstart`, may raise the
threshold while its gesture is under way, and calls `cancelPress()` when it
takes the press over.
**Caveats:** found by signature (`window-curvature` in `signatures.json`);
on mismatch the dashboard stays stock. Tested with SteamVR build 11008059.
@@ -742,8 +746,10 @@ steamFrame.dashboard.frameControls = {
```
With [window curvature](#window-curvature-dashboardwindowcurvature), a drag
before the ring shows adjusts curvature; once the ring shows, the long press
wins. Debugging: `window.__sfuiFrameControls.dump()`, `.placement()`,
on the curvature control adjusts curvature and cancels the long press, also
after the ring shows; once the ring shows, the drag needs 3× the usual travel
(`dragThresholdPixels`, counted from where the press started), so laser
drift during the hold doesn't cancel it. Debugging: `window.__sfuiFrameControls.dump()`, `.placement()`,
`.reset()` (forget choices), `.log`.
**Limitations:** laser only (no right-click or thumbstick click reaches the
+10 -5
View File
@@ -34,9 +34,13 @@
// swallowed so its stock action doesn't run.
// Contract with window-curvature (whose controls own press-and-drag): its
// elements carry class sfui-curv-ctl; a drag there dispatches a bubbling
// CustomEvent 'sfui-curv-dragstart' (cancels the long press before the ring
// shows); once the ring shows, the long press takes over the press via
// window.__sfuiWindowCurvature.cancelPress(). Neither reads the other's
// CustomEvent 'sfui-curv-dragstart', which cancels the long press (a drag
// always wins). When the ring shows, the press's drag threshold is raised to
// CURV_RING_THRESHOLD x its own via
// window.__sfuiWindowCurvature.scalePressDragThreshold(), still measured from
// the press start: laser drift keeps the long press, a deliberate drag still
// cancels it. On completion the long press takes the press over via
// cancelPress() (no curvature click on release). Neither reads the other's
// thresholds.
//
// Popup: its own scene-graph panel so the bar/menu panels don't change. The
@@ -59,7 +63,7 @@
// setPlacement(name or "icon:N", 'bar' | 'menu' | null), reset().
((find, sigs, opts) => {
const NAME = 'frame-controls';
const VERSION = 3;
const VERSION = 4;
const T_SPACER = 1, T_ACTION = 2;
const LS_KEY = 'sfui.frameControls.v1';
// Action icon enums (sigs.controls anchors them): names for the options.
@@ -494,6 +498,7 @@
// ---- long press --------------------------------------------------------------------
const SVGNS = 'http://www.w3.org/2000/svg';
const RING_DELAY = Math.min(1000, o.longPressMs / 2);
const CURV_RING_THRESHOLD = 3; // x window-curvature's drag threshold once the ring shows
const curvApi = () => window.__sfuiWindowCurvature;
let press = null; // { hit, t0, timer, ringTimer, ring, done, curv }
let swallow = null; // { until, button }: the click after a completed long press
@@ -575,7 +580,7 @@
hit.button.classList.add('sfui-fc-pressing'); // positioning context for the ring
p.ringTimer = setTimeout(() => {
if (press !== p || p.done) return;
if (p.curv) curvApi()?.cancelPress?.(); // the long press takes the press over
if (p.curv) curvApi()?.scalePressDragThreshold?.(CURV_RING_THRESHOLD); // drift keeps the hold, a drag still wins
p.ring = showRing(p);
}, RING_DELAY);
return;
+29 -14
View File
@@ -47,24 +47,31 @@
// Contract with other patches (e.g. frame-controls' long press); this patch
// owns press-and-drag on its controls:
// - Every element it drives has the class `sfui-curv-ctl`.
// - When a press becomes a drag (opts.dragThresholdPixels px vertical) it
// dispatches a bubbling CustomEvent `sfui-curv-dragstart` on the element
// (detail { frameID, where: 'menu' | 'bar' }); when that drag ends
// (release or cancelPress), `sfui-curv-dragend`.
// - When a press becomes a drag (opts.dragThresholdPixels px vertical from
// where the press started) it dispatches a bubbling CustomEvent
// `sfui-curv-dragstart` on the element (detail { frameID, where: 'menu' |
// 'bar' }); when that drag ends (release or cancelPress),
// `sfui-curv-dragend`.
// - window.__sfuiWindowCurvature.scalePressDragThreshold(factor) sets the
// current press's threshold to factor x opts.dragThresholdPixels, still
// measured from the press start, so small laser drift doesn't start a
// drag while a deliberate one still does; returns whether it applied
// (a press that is not yet a drag). The next press starts at 1x.
// - window.__sfuiWindowCurvature.cancelPress() ends the current press
// without its click (a drag keeps its value); returns whether a press
// was active.
// A patch with its own gesture on these elements lets mousemove through while
// undecided, drops its gesture on `sfui-curv-dragstart`, and calls
// cancelPress() when it takes the press over. Neither side reads the other's
// thresholds or restores the other's state.
// undecided, drops its gesture on `sfui-curv-dragstart`, may raise the
// threshold with scalePressDragThreshold() while its gesture is under way,
// and calls cancelPress() when it takes the press over. Neither side reads
// the other's thresholds or restores the other's state.
//
// Debugging: window.__sfuiWindowCurvature: dump(), hits (rewrite/haptic
// counters), log, setValue(frameID, v), controls(), cancelPress(),
// dragValue(v0, dy, ppu, cur).
// scalePressDragThreshold(factor), pressing, dragValue(v0, dy, ppu, cur).
((find, sigs, opts, hooks) => {
const NAME = 'window-curvature';
const VERSION = 19;
const VERSION = 20;
const FLAT = 999; // origin distances >= this are "flat"
const ICON_OFF = 40, ICON_ON = 39; // Toggle Curvature action icons (sigs.curvatureAction)
const HAPTIC = { Snap: 3, Sliding: 4, SlidingEdge: 5 }; // EOverlayHapticEffect (sigs.hapticEffects)
@@ -338,11 +345,12 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
// Presses/clicks are stopped at the element so React (root listener) never
// runs the stock onClick; a press that never became a drag is a click on
// mouseup. Drag is vertical and relative (the cylinder bends horizontally,
// so y barely moves as curvature changes); after dragThresholdPixels px it
// so y barely moves as curvature changes); past the press's threshold
// (dragThresholdPixels, or as scaled by scalePressDragThreshold) it
// re-bases (no jump). One press at a time, window capture listeners.
const stop = (e) => e.stopPropagation();
const STOPPED = ['click', 'dblclick', 'mouseup', 'pointerdown', 'pointerup', 'contextmenu'];
let press = null; // { c, y0, v0, last, moved }
let press = null; // { c, y0, v0, last, moved, threshold }
const emit = (c, type) => {
try {
@@ -359,7 +367,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
const y = e.clientY;
p.last = y;
if (!p.moved) {
if (Math.abs(y - p.y0) < o.dragThresholdPixels) return;
if (Math.abs(y - p.y0) < p.threshold) return;
p.moved = true;
muteStock = true;
p.c.el?.classList.add('sfui-dragging');
@@ -401,7 +409,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
const f = FS.GetFrame(c.fid);
if (e.button !== 0 || !f) return;
endPress('new press');
press = { c, y0: e.clientY, last: e.clientY, v0: shown(f), moved: false };
press = { c, y0: e.clientY, last: e.clientY, v0: shown(f), moved: false, threshold: o.dragThresholdPixels };
window.addEventListener('mousemove', onMove, true);
window.addEventListener('mouseup', onUp, true);
};
@@ -536,8 +544,15 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
version: VERSION, id, opts: o, hits, log: logBuf, state: S, teardown, check, dump, dragValue,
setValue: (fid, v) => { const f = FS.GetFrame(fid); if (f) setValue(f, v, 'api'); return f ? shown(f) : 'no frame'; },
cancelPress: () => endPress('cancelled'),
scalePressDragThreshold: (factor) => {
const p = press;
if (!p || p.moved || !(factor >= 0 && Number.isFinite(factor))) return false;
p.threshold = factor * o.dragThresholdPixels;
log('press drag threshold', { frame: p.c.fid, where: p.c.where, px: p.threshold });
return true;
},
controls: () => [...controls.values()].map((c) => ({ frame: c.fid, where: c.where, connected: !!c.el?.isConnected })),
get pressing() { return press && { frame: press.c.fid, where: press.c.where, dragging: press.moved }; },
get pressing() { return press && { frame: press.c.fid, where: press.c.where, dragging: press.moved, threshold: press.threshold }; },
};
window.__sfuiWindowCurvature = state;
log('patched', { version: VERSION, opts: o });