mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 05:00:24 +02:00
window-curvature: drag detents, muted hover haptics, longer drag per step
- Snap points are detents in drag distance (snapPixels, default 24): the value holds at the point, then continues, so no value is skipped. `snap` is removed in favour of `snapPixels`. - The dashboard's own hover haptics are muted during a drag, so only the value's ticks and detents are felt. - Longer drag per step by default: 120 px per 1.0 in the menu, 60 on the bar button (step stays 0.05).
This commit is contained in:
1 parent
772681a771
commit
2feb3d405d
3 files changed
+90
-46
No files matched your search
@@ -214,13 +214,13 @@ without it.
|
||||
| `steamFrame.dashboard.windowCurvature.default` | number | `1.0` | Curvature of curved world/hand windows without own value (1 = stock, 0 = flat). |
|
||||
| `steamFrame.dashboard.windowCurvature.max` | number | `3.0` | Largest curvature. |
|
||||
| `steamFrame.dashboard.windowCurvature.step` | number | `0.05` | Rounding step while dragging. |
|
||||
| `steamFrame.dashboard.windowCurvature.snap` | number | `0.15` | Snap distance around snap points; `0`: none. |
|
||||
| `steamFrame.dashboard.windowCurvature.snapPixels` | unsigned int (px) | `24` | Detent at each snap point in drag pixels: the value holds there, then continues (nothing skipped); `0`: none. |
|
||||
| `steamFrame.dashboard.windowCurvature.snapPoints` | list of numbers | `[ 0 1.0 ]` | Snap points (flat, stock). |
|
||||
| `steamFrame.dashboard.windowCurvature.dragThreshold` | unsigned int (px) | `8` | Vertical travel before a press becomes a drag. |
|
||||
| `steamFrame.dashboard.windowCurvature.dragPixelsPerUnit` | number (px) | `60` | Drag distance per 1.0 in the menu. |
|
||||
| `steamFrame.dashboard.windowCurvature.barDragPixelsPerUnit` | number (px) | `30` | Drag distance per 1.0 on the bar button. |
|
||||
| `steamFrame.dashboard.windowCurvature.dragPixelsPerUnit` | number (px) | `120` | Drag distance per 1.0 in the menu (6 px per 0.05 step). |
|
||||
| `steamFrame.dashboard.windowCurvature.barDragPixelsPerUnit` | number (px) | `60` | Drag distance per 1.0 on the bar button. |
|
||||
| `steamFrame.dashboard.windowCurvature.barDragRoom` | unsigned int (px) | `160` | Room added above/below the bar while dragging; `0`: none. |
|
||||
| `steamFrame.dashboard.windowCurvature.haptics` | bool | `true` | Controller haptics while dragging. |
|
||||
| `steamFrame.dashboard.windowCurvature.haptics` | bool | `true` | Controller haptics while dragging (steps, detents, edges); the dashboard's hover clicks are muted during a drag. |
|
||||
| `steamFrame.dashboard.frameControls.enable` | bool | `false` | Move window controls between bar and three-dot menu, see [Window control bar](#window-control-bar-dashboardframecontrols). |
|
||||
| `steamFrame.dashboard.frameControls.longPressMs` | int, 300-10000 (ms) | `1500` | Long-press duration. |
|
||||
| `steamFrame.dashboard.frameControls.inBar` | list of control names | `[ ]` | Controls that start in the bar: `keyboard`, `float`, `dashboard`, `theater`, `dockLeft`, `dockRight`, `close`, `curvature`, `"icon:<n>"`. |
|
||||
@@ -599,8 +599,9 @@ without the value, with haptic steps.
|
||||
|
||||
- **click:** curved → flat, flat → stock (1);
|
||||
- **drag up/down** with the laser: curvature from 0 (flat) to `max`,
|
||||
relative to stock (2 = half the radius), snapping near `snapPoints`, with
|
||||
haptics for snaps, edges and steps.
|
||||
relative to stock (2 = half the radius), with a detent of `snapPixels` of
|
||||
drag at each of `snapPoints` (no values skipped), and haptics for snaps,
|
||||
edges and steps.
|
||||
|
||||
A window without its own value is shown at `default` once curved in the
|
||||
world or on a hand, at 1 in the dashboard or theater. Values are kept per
|
||||
@@ -611,8 +612,9 @@ window until SteamVR restarts. Debugging: `window.__sfuiWindowCurvature.dump()`
|
||||
|
||||
- Laser only; with gamepad navigation the row is the stock toggle.
|
||||
- No thumbstick scrolling (SteamVR sends no wheel events to the menu).
|
||||
- The laser stops at the menu's edge, so `dragPixelsPerUnit` × `max` should
|
||||
stay below ~190 px to reach `max` in one drag.
|
||||
- The laser stops at the menu's edge (~190 px above the row): with the
|
||||
default 120 px per 1.0, 0 → 1 fits into one drag, 0 → 3 takes two. Lower
|
||||
`dragPixelsPerUnit` (≤ 60) for the full range in one drag.
|
||||
|
||||
**For patch authors** (other patches handling presses on these controls):
|
||||
every element the patch drives has class `sfui-curv-ctl`; when a press
|
||||
|
||||
@@ -12,7 +12,11 @@ let
|
||||
inherit default description;
|
||||
};
|
||||
in {
|
||||
imports = [ ./steam-ui-patches.nix ./steamvr-debugger.nix ];
|
||||
imports = [
|
||||
./steam-ui-patches.nix ./steamvr-debugger.nix
|
||||
(lib.mkRemovedOptionModule [ "steamFrame" "dashboard" "windowCurvature" "snap" ]
|
||||
"Snapping is a detent in drag pixels now: use steamFrame.dashboard.windowCurvature.snapPixels.")
|
||||
];
|
||||
|
||||
options.steamFrame.dashboard.windowCurvature = {
|
||||
enable = lib.mkEnableOption ''
|
||||
@@ -28,13 +32,19 @@ in {
|
||||
'';
|
||||
max = value 3.0 "Largest curvature the control goes to (relative to the stock curve).";
|
||||
step = value 0.05 "Step the value is rounded to while dragging.";
|
||||
snap = value 0.15 ''
|
||||
Snap distance around snap points while dragging; 0 = no snapping.
|
||||
'';
|
||||
snapPixels = mkOption {
|
||||
type = types.ints.unsigned;
|
||||
default = 24;
|
||||
description = ''
|
||||
Detent at each snap point, in pixels of drag: the value holds at the
|
||||
point for this much travel, then continues from it (no values are
|
||||
skipped). 0 = no detent.
|
||||
'';
|
||||
};
|
||||
snapPoints = mkOption {
|
||||
type = types.listOf types.number;
|
||||
default = [ 0 1.0 ];
|
||||
description = "Values the drag snaps to (0 = flat, 1 = stock curve).";
|
||||
description = "Values with a detent while dragging (0 = flat, 1 = stock curve).";
|
||||
};
|
||||
dragThreshold = mkOption {
|
||||
type = types.ints.unsigned;
|
||||
@@ -43,11 +53,12 @@ in {
|
||||
Vertical laser travel (menu px) that turns a press into a drag.
|
||||
'';
|
||||
};
|
||||
dragPixelsPerUnit = value 60 ''
|
||||
Menu pixels per 1.0 of curvature. The laser stops at the menu's edge
|
||||
(~190 px above the row), so 0 to `max` should fit.
|
||||
dragPixelsPerUnit = value 120 ''
|
||||
Menu pixels per 1.0 of curvature (6 px per 0.05 step). The laser stops
|
||||
at the menu's edge (~190 px above the row): with the default, 0 to 1
|
||||
fits into one drag, the full range to `max` = 3 takes two.
|
||||
'';
|
||||
barDragPixelsPerUnit = value 30 ''
|
||||
barDragPixelsPerUnit = value 60 ''
|
||||
Bar pixels per 1.0 of curvature on the bottom-bar button.
|
||||
'';
|
||||
barDragRoom = mkOption {
|
||||
@@ -62,7 +73,7 @@ in {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Controller haptics while dragging (snap points, 0/`max` edges, steps).
|
||||
Controller haptics while dragging (steps, detents, 0/`max` edges); the dashboard's own hover clicks are muted during a drag.
|
||||
'';
|
||||
};
|
||||
};
|
||||
@@ -78,8 +89,8 @@ in {
|
||||
message = "steamFrame.dashboard.windowCurvature.default must be within 0 .. max (${builtins.toJSON cfg.max}), got ${builtins.toJSON cfg.default}.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.snap >= 0 && lib.all (p: p >= 0 && p <= cfg.max) cfg.snapPoints;
|
||||
message = "steamFrame.dashboard.windowCurvature: snap must be >= 0 and snapPoints within 0 .. max (${builtins.toJSON cfg.max}).";
|
||||
assertion = lib.all (p: p >= 0 && p <= cfg.max) cfg.snapPoints;
|
||||
message = "steamFrame.dashboard.windowCurvature: snapPoints must be within 0 .. max (${builtins.toJSON cfg.max}).";
|
||||
}
|
||||
{
|
||||
assertion = cfg.dragPixelsPerUnit > 0 && cfg.barDragPixelsPerUnit > 0;
|
||||
@@ -94,7 +105,7 @@ in {
|
||||
patch = mkPatch {
|
||||
name = "window-curvature";
|
||||
src = ./window-curvature/patch.js;
|
||||
opts = removeAttrs cfg [ "enable" ];
|
||||
opts = removeAttrs cfg [ "enable" "snap" ];
|
||||
};
|
||||
unpatch = ./window-curvature/unpatch.js;
|
||||
} ];
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// window-curvature: adjustable curvature per SteamVR dashboard window. The
|
||||
// stock "Toggle Curvature" control becomes a wheel: in the More Options menu
|
||||
// row (value shown on the right) and, if the control is in the bottom bar
|
||||
// (e.g. via frame-controls), on that bar button (no value; steps and snap
|
||||
// points are felt as haptics). Click toggles (curved -> flat, flat -> stock
|
||||
// (e.g. via frame-controls), on that bar button (no value; steps and
|
||||
// detents are felt as haptics). Click toggles (curved -> flat, flat -> stock
|
||||
// curve); dragging up/down with the laser sets the curvature live.
|
||||
//
|
||||
// Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title
|
||||
// "systemui"). mkPatch patch (see lib/default.nix); opts: { default, max,
|
||||
// step, snap, snapPoints, dragThreshold, dragPixelsPerUnit,
|
||||
// step, snapPixels, snapPoints, dragThreshold, dragPixelsPerUnit,
|
||||
// barDragPixelsPerUnit, barDragRoom, haptics }.
|
||||
//
|
||||
// Stock curvature (frame.curvature, systemui's `curvature` component):
|
||||
@@ -36,9 +36,14 @@
|
||||
// origin is compensated in outgoing scene graphs so the bar stays put in VR.
|
||||
// Laser events still mapped with the old layout are detected and shifted.
|
||||
//
|
||||
// Dragging: value = start value + drag distance / px per unit, rounded to
|
||||
// step. Snap points are detents in drag distance: the value holds there for
|
||||
// opts.snapPixels of travel, then continues, so no value is skipped.
|
||||
//
|
||||
// Haptics (opts.haptics): VROverlay.TriggerOverlayHapticEffect with stock
|
||||
// EOverlayHapticEffect values: SlidingEdge at 0/max, Snap at snap points,
|
||||
// Sliding otherwise (at most every 30 ms).
|
||||
// Sliding per step (at most every 30 ms). During a drag the dashboard's own
|
||||
// haptics (hover clicks on buttons the laser passes) are muted.
|
||||
//
|
||||
// Contract with other patches (e.g. frame-controls' long press); this patch
|
||||
// owns press-and-drag on its controls:
|
||||
@@ -56,19 +61,20 @@
|
||||
// thresholds or restores the other's state.
|
||||
//
|
||||
// Debugging: window.__sfuiWindowCurvature: dump(), hits (rewrite/haptic
|
||||
// counters), log, setValue(frameID, v), controls(), cancelPress().
|
||||
// counters), log, setValue(frameID, v), controls(), cancelPress(),
|
||||
// dragValue(v0, dy, ppu, cur).
|
||||
((find, sigs, opts, hooks) => {
|
||||
const NAME = 'window-curvature';
|
||||
const VERSION = 11;
|
||||
const VERSION = 16;
|
||||
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)
|
||||
|
||||
const o = {
|
||||
default: 1, max: 3, step: 0.05, snap: 0.15, snapPoints: [0, 1], dragThreshold: 8, dragPixelsPerUnit: 60,
|
||||
barDragPixelsPerUnit: 30, barDragRoom: 160, haptics: true, ...opts,
|
||||
default: 1, max: 3, step: 0.05, snapPixels: 24, snapPoints: [0, 1], dragThreshold: 8, dragPixelsPerUnit: 120,
|
||||
barDragPixelsPerUnit: 60, barDragRoom: 160, haptics: true, ...opts,
|
||||
};
|
||||
if (!(o.max > 0 && o.step > 0 && o.step <= o.max && o.default >= 0 && o.default <= o.max && o.snap >= 0 &&
|
||||
if (!(o.max > 0 && o.step > 0 && o.step <= o.max && o.default >= 0 && o.default <= o.max && o.snapPixels >= 0 &&
|
||||
Array.isArray(o.snapPoints) && o.snapPoints.every((p) => p >= 0 && p <= o.max) &&
|
||||
o.dragThreshold >= 0 && o.dragPixelsPerUnit > 0 && o.barDragPixelsPerUnit > 0 &&
|
||||
Number.isInteger(o.barDragRoom) && o.barDragRoom >= 0 && typeof o.haptics === 'boolean'))
|
||||
@@ -119,27 +125,49 @@
|
||||
const curved = (f) => !!curvatureOf(f)?.shouldCurve;
|
||||
const shown = (f) => (curved(f) ? strength(f) : 0);
|
||||
|
||||
// Value for a drag position (target: unrounded value under the pointer),
|
||||
// or null to keep the current one: within +-snap of a snap point the value
|
||||
// is that point, else target rounded to step; a change of less than 0.6
|
||||
// steps away from cur is ignored (laser jitter at a step boundary).
|
||||
const dragValue = (target, cur) => {
|
||||
const snapped = o.snapPoints.find((p) => Math.abs(target - p) <= o.snap);
|
||||
const v = clamp(snapped ?? round(target));
|
||||
if (v === cur || (snapped === undefined && Math.abs(target - cur) < o.step * 0.6)) return null;
|
||||
return v;
|
||||
// Value for a drag of dy px (up = positive) from v0 at ppu px per unit, or
|
||||
// null to keep cur. Snap points are detents in drag distance: reaching one
|
||||
// (or starting on it) holds the value there for snapPixels of travel, then
|
||||
// it continues from the point, so no values are skipped. Off a snap point
|
||||
// the value is rounded to step; a change of less than 0.6 steps away from
|
||||
// cur is ignored (laser jitter at a step boundary).
|
||||
const dragValue = (v0, dy, ppu, cur) => {
|
||||
const dir = Math.sign(dy);
|
||||
let v = v0, rest = Math.abs(dy), held = null;
|
||||
const points = o.snapPoints.filter((p) => (dir > 0 ? p >= v0 : p <= v0)).sort((a, b) => dir * (a - b));
|
||||
for (const p of points) {
|
||||
const dist = Math.abs(p - v) * ppu;
|
||||
if (rest <= dist) break;
|
||||
rest -= dist; v = p;
|
||||
if (rest <= o.snapPixels) { held = p; rest = 0; break; }
|
||||
rest -= o.snapPixels;
|
||||
}
|
||||
const target = held ?? v + dir * rest / ppu;
|
||||
const nv = clamp(held ?? round(target));
|
||||
if (nv === cur || (held === null && Math.abs(target - cur) < o.step * 0.6)) return null;
|
||||
return nv;
|
||||
};
|
||||
|
||||
// ---- haptics -------------------------------------------------------------------------
|
||||
let lastHaptic = -Infinity;
|
||||
// While a drag runs, the dashboard's own haptics (a click whenever the laser
|
||||
// enters a button, e.g. menu rows the drag passes over) are muted, so only
|
||||
// the value's ticks and detents are felt. The stock function is wrapped
|
||||
// once (restored by teardown); our ticks call the original.
|
||||
let lastHaptic = -Infinity, muteStock = false;
|
||||
const ov = window.VRHTML?.VROverlay;
|
||||
const stockHaptic = typeof ov?.TriggerOverlayHapticEffect === 'function'
|
||||
? (ov.TriggerOverlayHapticEffect.__sfuiCurvOrig ?? ov.TriggerOverlayHapticEffect) : null;
|
||||
if (stockHaptic) {
|
||||
const wrapped = function (...a) { if (!muteStock) return stockHaptic.apply(this, a); };
|
||||
wrapped.__sfuiCurvOrig = stockHaptic;
|
||||
ov.TriggerOverlayHapticEffect = wrapped;
|
||||
}
|
||||
const haptic = (effect) => {
|
||||
if (!o.haptics) return;
|
||||
const ov = window.VRHTML?.VROverlay;
|
||||
if (typeof ov?.TriggerOverlayHapticEffect !== 'function' || typeof ov.ThisOverlayHandle !== 'function') return;
|
||||
if (!o.haptics || !stockHaptic || typeof ov.ThisOverlayHandle !== 'function') return;
|
||||
const now = performance.now();
|
||||
if (effect === HAPTIC.Sliding && now - lastHaptic < 30) return;
|
||||
lastHaptic = now;
|
||||
try { ov.TriggerOverlayHapticEffect(ov.ThisOverlayHandle(), effect); hits.haptics++; } catch (e) { log('haptic failed', String(e)); }
|
||||
try { stockHaptic.call(ov, ov.ThisOverlayHandle(), effect); hits.haptics++; } catch (e) { log('haptic failed', String(e)); }
|
||||
};
|
||||
const hapticFor = (v) => haptic(v <= 0 || v >= o.max ? HAPTIC.SlidingEdge
|
||||
: o.snapPoints.includes(v) ? HAPTIC.Snap : HAPTIC.Sliding);
|
||||
@@ -378,6 +406,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
||||
if (!p.moved) {
|
||||
if (Math.abs(y - p.y0) < o.dragThreshold) return;
|
||||
p.moved = true;
|
||||
muteStock = true;
|
||||
p.c.el?.classList.add('sfui-dragging');
|
||||
log('drag', { frame: p.c.fid, where: p.c.where });
|
||||
emit(p.c, 'sfui-curv-dragstart');
|
||||
@@ -387,7 +416,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
||||
return;
|
||||
}
|
||||
const ppu = p.c.where === 'bar' ? o.barDragPixelsPerUnit : o.dragPixelsPerUnit;
|
||||
const v = dragValue(p.v0 + (p.y0 - y) / ppu, shown(f));
|
||||
const v = dragValue(p.v0, p.y0 - y, ppu, shown(f));
|
||||
if (v === null) return;
|
||||
setValue(f, v, 'drag');
|
||||
hapticFor(v);
|
||||
@@ -398,6 +427,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
||||
// Ends the press; `release` of a press that never moved is a click.
|
||||
function endPress(why) {
|
||||
const p = press;
|
||||
muteStock = false;
|
||||
if (!p) return false;
|
||||
press = null;
|
||||
window.removeEventListener('mousemove', onMove, true);
|
||||
@@ -526,6 +556,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
||||
|
||||
const teardown = () => {
|
||||
endPress('teardown');
|
||||
if (stockHaptic && ov.TriggerOverlayHapticEffect?.__sfuiCurvOrig === stockHaptic) ov.TriggerOverlayHapticEffect = stockHaptic;
|
||||
for (const d of disposers.splice(0)) d();
|
||||
clearTimeout(syncTimer); clearTimeout(resendTimer);
|
||||
for (const key of [...controls.keys()]) removeControl(key);
|
||||
@@ -560,6 +591,6 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
||||
log('patched', { version: VERSION, opts: o });
|
||||
resend();
|
||||
queueSync();
|
||||
return `patched (default ${o.default}, max ${o.max}, step ${o.step}, snap ±${o.snap} at ${o.snapPoints.join('/')}, ` +
|
||||
return `patched (default ${o.default}, max ${o.max}, step ${o.step}, snap ${o.snapPixels} px at ${o.snapPoints.join('/')}, ` +
|
||||
`haptics ${o.haptics ? 'on' : 'off'})`;
|
||||
})
|
||||
Reference in new issue
Block a user