window-curvature: no bar flicker during a drag

The bar panel is no longer padded during a bar drag (its new size and the
compensated origin reached the compositor at different times, so the bar
flickered). SteamVR keeps sending the pressed bar's coordinates past its
edge, so the drag simply uses them. `barDragRoom` is removed.
This commit is contained in:
Pierre Kisters committed 2026-09-28 02:39:13 +02:00
1 parent 2feb3d405d
commit 817190facd
4 files changed
+17 -77

No files matched your search

-1
View File
@@ -219,7 +219,6 @@ without it.
| `steamFrame.dashboard.windowCurvature.dragThreshold` | unsigned int (px) | `8` | Vertical travel before a press becomes a drag. |
| `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 (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. |
-5
View File
@@ -331,11 +331,6 @@
"expects": ["properties.origin=", "properties.debug_name=this.props.debug_name", ".TopCenter:return{x:0,y:1}",
"this.m_Rect=this.getCurrentRootElement().getBoundingClientRect()", "new ResizeObserver("]
},
"sgApp": {
"checkOnly": true,
"module": { "includes": ["forceLayoutUpdate(){", "s_bPanelsAreDirty"] },
"expects": ["forceLayoutUpdate=this.forceLayoutUpdate", "updateLayoutValues()"]
},
"hapticEffects": {
"checkOnly": true,
"module": { "includes": ["e[e.Snap=3]=\"Snap\",e[e.Sliding=4]=\"Sliding\",e[e.SlidingEdge=5]=\"SlidingEdge\""] }
+3 -9
View File
@@ -14,6 +14,8 @@ let
in {
imports = [
./steam-ui-patches.nix ./steamvr-debugger.nix
(lib.mkRemovedOptionModule [ "steamFrame" "dashboard" "windowCurvature" "barDragRoom" ]
"Not needed: SteamVR keeps sending the bar's coordinates past its edge during a drag.")
(lib.mkRemovedOptionModule [ "steamFrame" "dashboard" "windowCurvature" "snap" ]
"Snapping is a detent in drag pixels now: use steamFrame.dashboard.windowCurvature.snapPixels.")
];
@@ -61,14 +63,6 @@ in {
barDragPixelsPerUnit = value 60 ''
Bar pixels per 1.0 of curvature on the bottom-bar button.
'';
barDragRoom = mkOption {
type = types.ints.unsigned;
default = 160;
description = ''
Transparent room (px) added above and below the bar while its button
is dragged, so the laser stays on the panel; 0 = none.
'';
};
haptics = mkOption {
type = types.bool;
default = true;
@@ -105,7 +99,7 @@ in {
patch = mkPatch {
name = "window-curvature";
src = ./window-curvature/patch.js;
opts = removeAttrs cfg [ "enable" "snap" ];
opts = removeAttrs cfg [ "enable" "snap" "barDragRoom" ];
};
unpatch = ./window-curvature/unpatch.js;
} ];
+14 -62
View File
@@ -8,7 +8,7 @@
// Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title
// "systemui"). mkPatch patch (see lib/default.nix); opts: { default, max,
// step, snapPixels, snapPoints, dragThreshold, dragPixelsPerUnit,
// barDragPixelsPerUnit, barDragRoom, haptics }.
// barDragPixelsPerUnit, haptics }.
//
// Stock curvature (frame.curvature, systemui's `curvature` component):
// shouldCurve = m_bCurveOverride (set by ToggleCurvature(), cleared on dock
@@ -29,12 +29,11 @@
// opts.default in the world or on a hand, 1 when docked in the dashboard or
// theater (so the Steam window stays concentric with the dashboard bar).
//
// Bar button: the bar panel (#legacy-frame-controls-<frameID>, origin
// TopCenter) is one button high and the laser stops at the pressed panel's
// edge. During a bar drag the panel gets opts.barDragRoom px of transparent
// padding above and below (window.forceLayoutUpdate() re-measures) and its
// 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.
// Bar button: the bar panel (#legacy-frame-controls-<frameID>) is one button
// high, but while the trigger is held SteamVR keeps sending the pressed
// panel's coordinates past its edge, so the drag uses them as they come. The
// bar panel is never resized or moved (padding it made it flicker: size and
// origin reach the compositor at different times).
//
// Dragging: value = start value + drag distance / px per unit, rounded to
// step. Snap points are detents in drag distance: the value holds there for
@@ -65,19 +64,19 @@
// dragValue(v0, dy, ppu, cur).
((find, sigs, opts, hooks) => {
const NAME = 'window-curvature';
const VERSION = 16;
const VERSION = 18;
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, snapPixels: 24, snapPoints: [0, 1], dragThreshold: 8, dragPixelsPerUnit: 120,
barDragPixelsPerUnit: 60, barDragRoom: 160, haptics: true, ...opts,
barDragPixelsPerUnit: 60, haptics: true, ...opts,
};
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'))
typeof o.haptics === 'boolean'))
return `invalid options ${JSON.stringify(opts)}`;
const id = JSON.stringify([VERSION, o]);
@@ -111,7 +110,7 @@
logBuf.push({ t: new Date().toISOString().slice(11, 23), msg, ...(data ? { data } : {}) });
if (logBuf.length > 200) logBuf.splice(0, logBuf.length - 200);
};
const hits = { sends: 0, origins: 0, scaled: 0, roomOrigins: 0, haptics: 0 };
const hits = { sends: 0, origins: 0, scaled: 0, haptics: 0 };
// ---- values ------------------------------------------------------------------------
const round = (v) => +(Math.round(v / o.step) * o.step).toFixed(6);
@@ -173,20 +172,6 @@
: o.snapPoints.includes(v) ? HAPTIC.Snap : HAPTIC.Sliding);
// ---- scene graph ---------------------------------------------------------------------
// Bar panels with drag room: frameID -> { node, pad, saved }.
const rooms = new Map();
// Keeps a padded bar panel's content where it was: the stock origin (x, y
// in -1..1 of the panel, y = 1 top) is re-expressed for the taller panel.
const fixOrigin = (p) => {
const m = /^legacy-frame-controls-(\d+)$/.exec(p.debug_name ?? '');
const r = m && rooms.get(Number(m[1]));
if (!r || !Array.isArray(p.origin) || p.origin.length < 2) return;
const h = r.node.getBoundingClientRect().height, inner = h - 2 * r.pad;
if (!(inner > 0)) return;
const fromTop = r.pad + ((1 - p.origin[1]) / 2) * inner;
p.origin = [p.origin[0], 1 - (2 * fromTop) / h, ...p.origin.slice(2)];
hits.roomOrigins++;
};
// Origin node ids may carry a "<prefix>::" (sub-scene); match the suffix.
const rewrite = (sg) => {
const byOrigin = new Map();
@@ -194,7 +179,7 @@
const c = curvatureOf(f);
if (c?.shouldCurve) byOrigin.set(c.curvatureTransformOriginID, f);
}
if (!byOrigin.size && !rooms.size) return;
if (!byOrigin.size) return;
const walk = (n) => {
if (!n || typeof n !== 'object') return;
if (Array.isArray(n)) { n.forEach(walk); return; }
@@ -209,7 +194,6 @@
hits.scaled++;
}
}
if (rooms.size && n.type === 'panel' && p) fixOrigin(p);
if (n.children) walk(n.children);
};
walk(sg);
@@ -230,12 +214,6 @@
try { resendStock(); } catch (e) { log('resend failed', String(e)); }
}, Math.max(0, lastResend + 40 - performance.now()));
};
// Panel bounds are re-measured on the stock forced layout update (a
// padding change doesn't trigger the panels' ResizeObserver).
const relayout = () => {
try { window.forceLayoutUpdate?.(); } catch (e) { log('forceLayoutUpdate failed', String(e)); }
resend();
};
// 0 = off, > 0 = on with that strength. On/off via stock ToggleCurvature so
// Steam's state and icon match. 0 forgets the stored value.
@@ -364,7 +342,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
// 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, shift, stale }
let press = null; // { c, y0, v0, last, moved }
const emit = (c, type) => {
try {
@@ -372,36 +350,13 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
} catch (e) { log(`${type} listener failed`, String(e)); }
};
const addRoom = (p) => {
const node = p.c.panel;
if (!o.barDragRoom || !node?.isConnected || rooms.has(p.c.fid)) return;
const before = p.c.el.getBoundingClientRect().top;
rooms.set(p.c.fid, { node, pad: o.barDragRoom, saved: [node.style.paddingTop, node.style.paddingBottom] });
node.style.paddingTop = node.style.paddingBottom = `${o.barDragRoom}px`;
const shift = p.c.el.getBoundingClientRect().top - before;
p.shift = shift; p.stale = shift !== 0; p.last += shift; p.y0 += shift;
relayout();
};
const removeRoom = (fid) => {
const r = rooms.get(fid);
if (!r) return;
rooms.delete(fid);
[r.node.style.paddingTop, r.node.style.paddingBottom] = r.saved;
relayout();
};
const onMove = (e) => {
const p = press;
if (!p) return;
const f = FS.GetFrame(p.c.fid);
if (!f) { endPress('frame gone'); return; }
let y = e.clientY;
// After the bar got its room, events the compositor still maps with the
// old layout are off by `shift`: take the reading closer to the last one.
if (p.stale) {
if (Math.abs(y + p.shift - p.last) < Math.abs(y - p.last)) y += p.shift;
else p.stale = false;
}
const y = e.clientY;
p.last = y;
if (!p.moved) {
if (Math.abs(y - p.y0) < o.dragThreshold) return;
@@ -411,7 +366,6 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
log('drag', { frame: p.c.fid, where: p.c.where });
emit(p.c, 'sfui-curv-dragstart');
if (press !== p) return; // a listener cancelled the press
if (p.c.where === 'bar') addRoom(p);
p.y0 = p.last;
return;
}
@@ -433,7 +387,6 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
window.removeEventListener('mousemove', onMove, true);
window.removeEventListener('mouseup', onUp, true);
p.c.el?.classList.remove('sfui-dragging');
removeRoom(p.c.fid);
if (p.moved) emit(p.c, 'sfui-curv-dragend');
const f = FS.GetFrame(p.c.fid);
if (why === 'release') {
@@ -448,7 +401,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, shift: 0, stale: false };
press = { c, y0: e.clientY, last: e.clientY, v0: shown(f), moved: false };
window.addEventListener('mousemove', onMove, true);
window.addEventListener('mouseup', onUp, true);
};
@@ -560,7 +513,6 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
for (const d of disposers.splice(0)) d();
clearTimeout(syncTimer); clearTimeout(resendTimer);
for (const key of [...controls.keys()]) removeControl(key);
for (const fid of [...rooms.keys()]) removeRoom(fid);
document.getElementById(STYLE_ID)?.remove();
hooks.remove(proto, 'SendMessage', NAME);
if (window.__sfuiWindowCurvature === state) delete window.__sfuiWindowCurvature;