mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 04:04:16 +02:00
window-curvature: wheel on the bar button, haptics
This commit is contained in:
1 parent
5eb1d07d01
commit
9993cbd4ab
6 files changed
+382
-128
No files matched your search
@@ -203,7 +203,7 @@ menu list every desktop entry, including terminals such as Konsole.
|
|||||||
| `steamFrame.dashboard.windowMaxScale` | null or number | `null` | Largest resize-handle scale of SteamVR dashboard windows, relative to their default size. `null`: stock (2). See [Dashboard windows](#dashboard-windows-dashboard). |
|
| `steamFrame.dashboard.windowMaxScale` | null or number | `null` | Largest resize-handle scale of SteamVR dashboard windows, relative to their default size. `null`: stock (2). See [Dashboard windows](#dashboard-windows-dashboard). |
|
||||||
| `steamFrame.dashboard.windowDistance.{world,theater,dashboard}.{min,max}` | null or number (m) | `null` | How close / far grabbed windows can be pulled in / pushed back. `null`: stock (world 0.25-5, theater 1-6, dashboard 0.3-4 m). |
|
| `steamFrame.dashboard.windowDistance.{world,theater,dashboard}.{min,max}` | null or number (m) | `null` | How close / far grabbed windows can be pulled in / pushed back. `null`: stock (world 0.25-5, theater 1-6, dashboard 0.3-4 m). |
|
||||||
| `steamFrame.dashboard.steamCloseButton.enable` | bool | `false` | Close (X) button on the dashboard's Steam window: switches to the previous window, or leaves just the dashboard bar. See [Steam close button](#steam-close-button-dashboardsteamclosebuttonenable). |
|
| `steamFrame.dashboard.steamCloseButton.enable` | bool | `false` | Close (X) button on the dashboard's Steam window: switches to the previous window, or leaves just the dashboard bar. See [Steam close button](#steam-close-button-dashboardsteamclosebuttonenable). |
|
||||||
| `steamFrame.dashboard.windowCurvature.enable` | bool | `false` | Adjustable curvature per dashboard window: the "Toggle Curvature" row of a window's More Options menu becomes a control (click: toggle, drag up/down: curvature). See [Window curvature](#window-curvature-dashboardwindowcurvature). |
|
| `steamFrame.dashboard.windowCurvature.enable` | bool | `false` | Adjustable curvature per dashboard window: the "Toggle Curvature" row of a window's More Options menu (and its bottom-bar button, when it sits there) becomes a control (click: toggle, drag up/down: curvature). See [Window curvature](#window-curvature-dashboardwindowcurvature). |
|
||||||
| `steamFrame.dashboard.windowCurvature.default` | number | `1.0` | Curvature of world/hand windows without a value of their own, once curved (relative to SteamVR's stock curve: 1 = stock, 2 = twice as curved, 0 = flat). Dashboard/theater windows start at 1. |
|
| `steamFrame.dashboard.windowCurvature.default` | number | `1.0` | Curvature of world/hand windows without a value of their own, once curved (relative to SteamVR's stock curve: 1 = stock, 2 = twice as curved, 0 = flat). Dashboard/theater windows start at 1. |
|
||||||
| `steamFrame.dashboard.windowCurvature.max` | number | `3.0` | Largest curvature the control goes to. |
|
| `steamFrame.dashboard.windowCurvature.max` | number | `3.0` | Largest curvature the control goes to. |
|
||||||
| `steamFrame.dashboard.windowCurvature.step` | number | `0.05` | Step the value is rounded to while dragging. |
|
| `steamFrame.dashboard.windowCurvature.step` | number | `0.05` | Step the value is rounded to while dragging. |
|
||||||
@@ -211,6 +211,9 @@ menu list every desktop entry, including terminals such as Konsole.
|
|||||||
| `steamFrame.dashboard.windowCurvature.snapPoints` | list of numbers | `[ 0 1.0 ]` | Snap points (flat, stock). |
|
| `steamFrame.dashboard.windowCurvature.snapPoints` | list of numbers | `[ 0 1.0 ]` | Snap points (flat, stock). |
|
||||||
| `steamFrame.dashboard.windowCurvature.dragThreshold` | unsigned int (px) | `8` | Vertical laser travel before a press becomes a drag instead of a click. |
|
| `steamFrame.dashboard.windowCurvature.dragThreshold` | unsigned int (px) | `8` | Vertical laser travel before a press becomes a drag instead of a click. |
|
||||||
| `steamFrame.dashboard.windowCurvature.dragPixelsPerUnit` | number (px) | `60` | Drag distance per 1.0 of curvature (the laser stops at the menu's edge, ~190 px above the row). |
|
| `steamFrame.dashboard.windowCurvature.dragPixelsPerUnit` | number (px) | `60` | Drag distance per 1.0 of curvature (the laser stops at the menu's edge, ~190 px above the row). |
|
||||||
|
| `steamFrame.dashboard.windowCurvature.barDragPixelsPerUnit` | number (px) | `30` | Drag distance per 1.0 of curvature on the bottom-bar button. |
|
||||||
|
| `steamFrame.dashboard.windowCurvature.barDragRoom` | unsigned int (px) | `160` | Transparent room added above and below a window's bottom bar while its curvature button is dragged, so the laser stays on the bar. `0`: none. |
|
||||||
|
| `steamFrame.dashboard.windowCurvature.haptics` | bool | `true` | Controller haptics while dragging: snap at snap points, edge at 0 and `max`, a tick per other step. |
|
||||||
| `steamFrame.steamvrDebugger.enable` | bool | `true` if a UI patch uses port 8087, else `false` | SteamVR dashboard DevTools on `127.0.0.1:8087` (`VRWebHelper/DebuggerEnabled`), needed by dashboard patches. See [SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable). |
|
| `steamFrame.steamvrDebugger.enable` | bool | `true` if a UI patch uses port 8087, else `false` | SteamVR dashboard DevTools on `127.0.0.1:8087` (`VRWebHelper/DebuggerEnabled`), needed by dashboard patches. See [SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable). |
|
||||||
| `steamFrame.hiddenApps` | list of str | `[ ]` | Desktop entry ids (without `.desktop`) to hide from the "+" and KDE menus. |
|
| `steamFrame.hiddenApps` | list of str | `[ ]` | Desktop entry ids (without `.desktop`) to hide from the "+" and KDE menus. |
|
||||||
| `steamFrame.clipboardSync.enable` | bool | `true` | Clipboard bridge between the Steam session and the nested desktop. |
|
| `steamFrame.clipboardSync.enable` | bool | `true` | Clipboard bridge between the Steam session and the nested desktop. |
|
||||||
@@ -748,13 +751,23 @@ flat.
|
|||||||
`127.0.0.1:8087`, title `systemui`; it turns on the
|
`127.0.0.1:8087`, title `systemui`; it turns on the
|
||||||
[SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable)) makes the whole
|
[SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable)) makes the whole
|
||||||
"Toggle Curvature" row a control, with the window's value on its right and
|
"Toggle Curvature" row a control, with the window's value on its right and
|
||||||
small arrows above/below it:
|
small arrows above/below it. When the control sits in the window's bottom
|
||||||
|
bar instead (e.g. moved there by a patch of your own), that button works
|
||||||
|
the same way, without the value: the steps are felt as haptics.
|
||||||
|
|
||||||
- **click**: curved → flat, flat → stock curve (1);
|
- **click**: curved → flat, flat → stock curve (1);
|
||||||
- **press and drag up/down** with the laser (after `dragThreshold` px): the
|
- **press and drag up/down** with the laser (after `dragThreshold` px): the
|
||||||
curvature follows live, `dragPixelsPerUnit` px per 1.0, rounded to `step`,
|
curvature follows live, `dragPixelsPerUnit` px per 1.0, rounded to `step`,
|
||||||
from 0 (flat) to `max`. Near a snap point (`snapPoints`, by default 0 and
|
from 0 (flat) to `max`. Near a snap point (`snapPoints`, by default 0 and
|
||||||
1, within ± `snap`) it snaps to it exactly; dragging on moves past.
|
1, within ± `snap`) it snaps to it exactly; dragging on moves past.
|
||||||
|
With `haptics`, the controller gives a snap at a snap point, an edge
|
||||||
|
bump at 0 and `max` and a light tick for other steps (SteamVR's own
|
||||||
|
overlay haptic effects).
|
||||||
|
- on the **bar button**, `barDragPixelsPerUnit` px per 1.0; the bar panel is
|
||||||
|
only one button high and the laser's position stops at the edge of the
|
||||||
|
pressed panel, so while dragging the panel gets `barDragRoom` px of
|
||||||
|
transparent room above and below (its origin is shifted in the scene
|
||||||
|
graph by the same amount, so the bar stays in place).
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
steamFrame.dashboard.windowCurvature = {
|
steamFrame.dashboard.windowCurvature = {
|
||||||
@@ -762,6 +775,7 @@ steamFrame.dashboard.windowCurvature = {
|
|||||||
# default = 1.0; max = 3.0; step = 0.05;
|
# default = 1.0; max = 3.0; step = 0.05;
|
||||||
# snap = 0.15; snapPoints = [ 0 1.0 ];
|
# snap = 0.15; snapPoints = [ 0 1.0 ];
|
||||||
# dragThreshold = 8; dragPixelsPerUnit = 60;
|
# dragThreshold = 8; dragPixelsPerUnit = 60;
|
||||||
|
# barDragPixelsPerUnit = 30; barDragRoom = 160; haptics = true;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -790,6 +804,17 @@ kept per window (its overlay key) in the `systemui` page
|
|||||||
dashboard page). `window.__sfuiWindowCurvature.dump()` lists every window's
|
dashboard page). `window.__sfuiWindowCurvature.dump()` lists every window's
|
||||||
state, `.log` recent events.
|
state, `.log` recent events.
|
||||||
|
|
||||||
|
**Other patches** that handle presses on the same controls (e.g. a long
|
||||||
|
press on frame controls) coordinate through a small contract instead of
|
||||||
|
guessing each other's timing: every element the patch drives has the class
|
||||||
|
`sfui-curv-ctl`; when a press on one becomes a drag, a bubbling
|
||||||
|
`CustomEvent` `sfui-curv-dragstart` (detail `{ frameID, where: 'menu' | 'bar' }`)
|
||||||
|
is dispatched on it, and `sfui-curv-dragend` when the drag ends;
|
||||||
|
`window.__sfuiWindowCurvature.cancelPress()` ends a press without its
|
||||||
|
click. A patch with its own gesture there lets `mousemove` through while
|
||||||
|
that gesture is undecided, drops it on `sfui-curv-dragstart`, and calls
|
||||||
|
`cancelPress()` when it takes the press over.
|
||||||
|
|
||||||
**Limitations:**
|
**Limitations:**
|
||||||
|
|
||||||
- Only the laser adjusts the value: with controller (gamepad) navigation of
|
- Only the laser adjusts the value: with controller (gamepad) navigation of
|
||||||
|
|||||||
@@ -295,7 +295,8 @@
|
|||||||
"checkOnly": true,
|
"checkOnly": true,
|
||||||
"module": { "includes": ["legacy-frame-controls-additional-options-", "controlAdditionalOptionsItems.filter("] },
|
"module": { "includes": ["legacy-frame-controls-additional-options-", "controlAdditionalOptionsItems.filter("] },
|
||||||
"expects": ["id:`legacy-frame-controls-additional-options-${", "isControlAdditionalOptionsOpen",
|
"expects": ["id:`legacy-frame-controls-additional-options-${", "isControlAdditionalOptionsOpen",
|
||||||
".AdditionalOptionsRow,", ".AdditionalOptionsLabel},", "display_name", "invocation", "onClick:()=>{", "onMouseDown:"]
|
".AdditionalOptionsRow,", ".AdditionalOptionsLabel},", "display_name", "invocation", "onClick:()=>{", "onMouseDown:",
|
||||||
|
"id:`legacy-frame-controls-${", "items_for_bottom_frame_controls", "control:e,", ".TopCenter,meters_per_pixel:"]
|
||||||
},
|
},
|
||||||
"curvatureAction": {
|
"curvatureAction": {
|
||||||
"checkOnly": true,
|
"checkOnly": true,
|
||||||
@@ -306,7 +307,8 @@
|
|||||||
"checkOnly": true,
|
"checkOnly": true,
|
||||||
"module": { "includes": ["e[e.Alive=2]=\"Alive\""] },
|
"module": { "includes": ["e[e.Alive=2]=\"Alive\""] },
|
||||||
"expects": ["this.curvature=", "this.docking=", "get isControlAdditionalOptionsOpen()",
|
"expects": ["this.curvature=", "this.docking=", "get isControlAdditionalOptionsOpen()",
|
||||||
"get controlAdditionalOptionsItems()", "get associatedSummonOverlayKeys()"]
|
"get controlAdditionalOptionsItems()", "get associatedSummonOverlayKeys()",
|
||||||
|
"get protoForSteam()", "isCurrentlyVisible"]
|
||||||
},
|
},
|
||||||
"docking": {
|
"docking": {
|
||||||
"checkOnly": true,
|
"checkOnly": true,
|
||||||
@@ -325,7 +327,22 @@
|
|||||||
},
|
},
|
||||||
"panel": {
|
"panel": {
|
||||||
"checkOnly": true,
|
"checkOnly": true,
|
||||||
"module": { "includes": ["properties[\"curvature-origin-id\"]"] }
|
"module": { "includes": ["properties[\"curvature-origin-id\"]"] },
|
||||||
|
"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\""] }
|
||||||
|
},
|
||||||
|
"hapticCall": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["TriggerOverlayHapticEffect(VRHTML.VROverlay.ThisOverlayHandle(),"] }
|
||||||
},
|
},
|
||||||
"sceneGraphNodes": {
|
"sceneGraphNodes": {
|
||||||
"checkOnly": true,
|
"checkOnly": true,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Adjustable curvature per SteamVR dashboard window
|
# Adjustable curvature per SteamVR dashboard window
|
||||||
# (steamFrame.dashboard.windowCurvature): the "Toggle Curvature" row of a
|
# (steamFrame.dashboard.windowCurvature): the "Toggle Curvature" control of a
|
||||||
# window's More Options menu becomes a control (click: toggle, drag up/down:
|
# window (row of its More Options menu, or its bottom-bar button when moved
|
||||||
# curvature). Runtime patch of SteamVR's dashboard page (vrwebhelper
|
# there) becomes a wheel (click: toggle, drag up/down: curvature, with
|
||||||
|
# controller haptics). Runtime patch of SteamVR's dashboard page (vrwebhelper
|
||||||
# "systemui", DevTools 127.0.0.1:8087; see window-curvature/patch.js for how
|
# "systemui", DevTools 127.0.0.1:8087; see window-curvature/patch.js for how
|
||||||
# it works), registered with steam-ui-patches.nix only when enabled; it turns
|
# it works), registered with steam-ui-patches.nix only when enabled; it turns
|
||||||
# on the SteamVR web helper debugger (steamvr-debugger.nix), which needs one
|
# on the SteamVR web helper debugger (steamvr-debugger.nix), which needs one
|
||||||
@@ -25,8 +26,10 @@ in {
|
|||||||
enable = lib.mkEnableOption ''
|
enable = lib.mkEnableOption ''
|
||||||
adjustable curvature per SteamVR dashboard window: the "Toggle
|
adjustable curvature per SteamVR dashboard window: the "Toggle
|
||||||
Curvature" row of a window's More Options (three-dot) menu shows the
|
Curvature" row of a window's More Options (three-dot) menu shows the
|
||||||
window's curvature and becomes a control. Click: curved → flat, flat →
|
window's curvature and becomes a control, and so does its button in
|
||||||
stock curve. Press and drag up/down with the laser: set the curvature
|
the window's bottom bar when it sits there (no value shown; steps and
|
||||||
|
snap points are felt as haptics). Click: curved → flat, flat → stock
|
||||||
|
curve. Press and drag up/down with the laser: set the curvature
|
||||||
live. Values are relative to SteamVR's stock curve (1 = stock, 2 = twice
|
live. Values are relative to SteamVR's stock curve (1 = stock, 2 = twice
|
||||||
as curved, i.e. half the radius, 0 = flat) and kept per window until
|
as curved, i.e. half the radius, 0 = flat) and kept per window until
|
||||||
SteamVR restarts. Runtime patch of the SteamVR dashboard
|
SteamVR restarts. Runtime patch of the SteamVR dashboard
|
||||||
@@ -63,6 +66,27 @@ in {
|
|||||||
stops at the menu's edge (about 190 px above the row), so 0 to `max`
|
stops at the menu's edge (about 190 px above the row), so 0 to `max`
|
||||||
should fit into that.
|
should fit into that.
|
||||||
'';
|
'';
|
||||||
|
barDragPixelsPerUnit = value 30 ''
|
||||||
|
Drag distance (bar pixels) per 1.0 of curvature on the bottom-bar
|
||||||
|
button (when the control sits in the bar).
|
||||||
|
'';
|
||||||
|
barDragRoom = mkOption {
|
||||||
|
type = types.ints.unsigned;
|
||||||
|
default = 160;
|
||||||
|
description = ''
|
||||||
|
Transparent room (pixels) added above and below a window's bottom bar
|
||||||
|
while its curvature button is dragged, so the laser stays on the bar
|
||||||
|
panel (its position stops at the pressed panel's edge). 0 = none.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
haptics = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Controller haptics while dragging: a snap at snap points, a stronger
|
||||||
|
edge at 0 and `max`, a light tick for other steps.
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
@@ -80,8 +104,8 @@ in {
|
|||||||
message = "steamFrame.dashboard.windowCurvature: snap must be >= 0 and snapPoints within 0 .. max (${builtins.toJSON cfg.max}).";
|
message = "steamFrame.dashboard.windowCurvature: snap must be >= 0 and snapPoints within 0 .. max (${builtins.toJSON cfg.max}).";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
assertion = cfg.dragPixelsPerUnit > 0;
|
assertion = cfg.dragPixelsPerUnit > 0 && cfg.barDragPixelsPerUnit > 0;
|
||||||
message = "steamFrame.dashboard.windowCurvature.dragPixelsPerUnit must be positive.";
|
message = "steamFrame.dashboard.windowCurvature: dragPixelsPerUnit and barDragPixelsPerUnit must be positive.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+295
-109
@@ -1,8 +1,11 @@
|
|||||||
// window-curvature: adjustable curvature per SteamVR dashboard window. The
|
// window-curvature: adjustable curvature per SteamVR dashboard window. The
|
||||||
// "Toggle Curvature" row of a window's More Options (three-dot) menu becomes
|
// stock "Toggle Curvature" control becomes a wheel wherever it is shown: the
|
||||||
// the control: click toggles (curved -> flat, flat -> stock curve), dragging
|
// row of a window's More Options (three-dot) menu (with the value on the
|
||||||
// up/down with the laser sets the curvature live; the value is shown on the
|
// right of the row) and, when the control sits in the window's bottom bar
|
||||||
// right of the row.
|
// (e.g. moved there by another patch), that bar button (no value shown:
|
||||||
|
// the steps and snap points are felt as controller haptics). Click toggles
|
||||||
|
// (curved -> flat, flat -> stock curve), dragging up/down with the laser sets
|
||||||
|
// the curvature live.
|
||||||
//
|
//
|
||||||
// Target: SteamVR's dashboard page (vrwebhelper, DevTools 127.0.0.1:8087,
|
// Target: SteamVR's dashboard page (vrwebhelper, DevTools 127.0.0.1:8087,
|
||||||
// title "systemui"). This file is a function expression, called by the file
|
// title "systemui"). This file is a function expression, called by the file
|
||||||
@@ -40,19 +43,57 @@
|
|||||||
// docked in the dashboard or the theater, so the docked Steam window stays
|
// docked in the dashboard or the theater, so the docked Steam window stays
|
||||||
// concentric with the dashboard bar.
|
// concentric with the dashboard bar.
|
||||||
//
|
//
|
||||||
|
// Bar button: the bar panel (vsg-node#legacy-frame-controls-<frameID>,
|
||||||
|
// origin TopCenter) is one button high, and the laser's position stops at
|
||||||
|
// the edge of the pressed panel. While a drag on the bar button runs, the
|
||||||
|
// panel gets opts.barDragRoom px of transparent padding above and below
|
||||||
|
// (stock window.forceLayoutUpdate() re-measures it), and the panel's origin
|
||||||
|
// is moved by the same amount in outgoing scene graphs, so the bar stays
|
||||||
|
// where it is in VR. The padding moves the bar down in page coordinates;
|
||||||
|
// laser events the compositor still maps with the old layout are recognised
|
||||||
|
// (the laser moves continuously) and shifted. opts.barDragPixelsPerUnit
|
||||||
|
// sets the drag speed there.
|
||||||
|
//
|
||||||
|
// Haptics (opts.haptics): VRHTML.VROverlay.TriggerOverlayHapticEffect with
|
||||||
|
// the stock EOverlayHapticEffect values (sigs.hapticEffects): SlidingEdge at
|
||||||
|
// 0 and max, Snap at a snap point, Sliding for other steps (at most every
|
||||||
|
// 30 ms).
|
||||||
|
//
|
||||||
|
// Contract with other patches (e.g. a long press on frame controls): this
|
||||||
|
// patch owns press-and-drag on its controls.
|
||||||
|
// - Every element it drives has the class `sfui-curv-ctl`.
|
||||||
|
// - When a press on one becomes a drag (opts.dragThreshold 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`.
|
||||||
|
// - window.__sfuiWindowCurvature.cancelPress() ends the current press on
|
||||||
|
// any of its controls without its click (a drag stays at its value);
|
||||||
|
// returns whether a press was active.
|
||||||
|
// A patch with its own gesture on these elements lets mousemove through
|
||||||
|
// while that gesture is undecided, drops it 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 afterwards.
|
||||||
|
//
|
||||||
// Debugging: window.__sfuiWindowCurvature: dump() (per frame: dock, on/off,
|
// Debugging: window.__sfuiWindowCurvature: dump() (per frame: dock, on/off,
|
||||||
// stored/applied value, distances), hits (scene-graph rewrite counters),
|
// stored/applied value, distances, controls), hits (scene-graph rewrite and
|
||||||
// log (last events, also in the state), setValue(frameID, v).
|
// haptic counters), log (last events, also in the state), setValue(frameID,
|
||||||
|
// v), controls(), cancelPress().
|
||||||
((find, sigs, opts, hooks) => {
|
((find, sigs, opts, hooks) => {
|
||||||
const NAME = 'window-curvature';
|
const NAME = 'window-curvature';
|
||||||
const VERSION = 10;
|
const VERSION = 11;
|
||||||
const FLAT = 999; // origin distances >= this are "flat"
|
const FLAT = 999; // origin distances >= this are "flat"
|
||||||
const ICON_OFF = 40, ICON_ON = 39; // Toggle Curvature action icons (sigs.curvatureAction)
|
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, ...opts };
|
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,
|
||||||
|
};
|
||||||
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.snap >= 0 &&
|
||||||
Array.isArray(o.snapPoints) && o.snapPoints.every((p) => p >= 0 && p <= o.max) &&
|
Array.isArray(o.snapPoints) && o.snapPoints.every((p) => p >= 0 && p <= o.max) &&
|
||||||
o.dragThreshold >= 0 && o.dragPixelsPerUnit > 0)) return `invalid options ${JSON.stringify(opts)}`;
|
o.dragThreshold >= 0 && o.dragPixelsPerUnit > 0 && o.barDragPixelsPerUnit > 0 &&
|
||||||
|
Number.isInteger(o.barDragRoom) && o.barDragRoom >= 0 && typeof o.haptics === 'boolean'))
|
||||||
|
return `invalid options ${JSON.stringify(opts)}`;
|
||||||
const id = JSON.stringify([VERSION, o]);
|
const id = JSON.stringify([VERSION, o]);
|
||||||
|
|
||||||
const prev = window.__sfuiWindowCurvature;
|
const prev = window.__sfuiWindowCurvature;
|
||||||
@@ -85,7 +126,7 @@
|
|||||||
logBuf.push({ t: new Date().toISOString().slice(11, 23), msg, ...(data ? { data } : {}) });
|
logBuf.push({ t: new Date().toISOString().slice(11, 23), msg, ...(data ? { data } : {}) });
|
||||||
if (logBuf.length > 200) logBuf.splice(0, logBuf.length - 200);
|
if (logBuf.length > 200) logBuf.splice(0, logBuf.length - 200);
|
||||||
};
|
};
|
||||||
const hits = { sends: 0, origins: 0, scaled: 0 };
|
const hits = { sends: 0, origins: 0, scaled: 0, roomOrigins: 0, haptics: 0 };
|
||||||
|
|
||||||
// ---- values ------------------------------------------------------------------------
|
// ---- values ------------------------------------------------------------------------
|
||||||
const round = (v) => +(Math.round(v / o.step) * o.step).toFixed(6);
|
const round = (v) => +(Math.round(v / o.step) * o.step).toFixed(6);
|
||||||
@@ -110,7 +151,35 @@
|
|||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- haptics -------------------------------------------------------------------------
|
||||||
|
let lastHaptic = -Infinity;
|
||||||
|
const haptic = (effect) => {
|
||||||
|
if (!o.haptics) return;
|
||||||
|
const ov = window.VRHTML?.VROverlay;
|
||||||
|
if (typeof ov?.TriggerOverlayHapticEffect !== 'function' || 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)); }
|
||||||
|
};
|
||||||
|
const hapticFor = (v) => haptic(v <= 0 || v >= o.max ? HAPTIC.SlidingEdge
|
||||||
|
: o.snapPoints.includes(v) ? HAPTIC.Snap : HAPTIC.Sliding);
|
||||||
|
|
||||||
// ---- scene graph ---------------------------------------------------------------------
|
// ---- 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.
|
// Origin node ids may carry a "<prefix>::" (sub-scene); match the suffix.
|
||||||
const rewrite = (sg) => {
|
const rewrite = (sg) => {
|
||||||
const byOrigin = new Map();
|
const byOrigin = new Map();
|
||||||
@@ -118,7 +187,7 @@
|
|||||||
const c = curvatureOf(f);
|
const c = curvatureOf(f);
|
||||||
if (c?.shouldCurve) byOrigin.set(c.curvatureTransformOriginID, f);
|
if (c?.shouldCurve) byOrigin.set(c.curvatureTransformOriginID, f);
|
||||||
}
|
}
|
||||||
if (!byOrigin.size) return;
|
if (!byOrigin.size && !rooms.size) return;
|
||||||
const walk = (n) => {
|
const walk = (n) => {
|
||||||
if (!n || typeof n !== 'object') return;
|
if (!n || typeof n !== 'object') return;
|
||||||
if (Array.isArray(n)) { n.forEach(walk); return; }
|
if (Array.isArray(n)) { n.forEach(walk); return; }
|
||||||
@@ -133,6 +202,7 @@
|
|||||||
hits.scaled++;
|
hits.scaled++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (rooms.size && n.type === 'panel' && p) fixOrigin(p);
|
||||||
if (n.children) walk(n.children);
|
if (n.children) walk(n.children);
|
||||||
};
|
};
|
||||||
walk(sg);
|
walk(sg);
|
||||||
@@ -153,12 +223,18 @@
|
|||||||
try { resendStock(); } catch (e) { log('resend failed', String(e)); }
|
try { resendStock(); } catch (e) { log('resend failed', String(e)); }
|
||||||
}, Math.max(0, lastResend + 40 - performance.now()));
|
}, 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();
|
||||||
|
};
|
||||||
|
|
||||||
// Sets the shown value of a frame: 0 = curvature off, > 0 = on with that
|
// Sets the shown value of a frame: 0 = curvature off, > 0 = on with that
|
||||||
// strength. On/off goes through the stock ToggleCurvature (what the row's
|
// strength. On/off goes through the stock ToggleCurvature (what the
|
||||||
// stock click invokes), so Steam's state and the row icon match. Going to
|
// control's stock click invokes), so Steam's state and the icon match.
|
||||||
// 0 forgets the window's value (turning it on again, e.g. by docking it in
|
// Going to 0 forgets the window's value (turning it on again, e.g. by
|
||||||
// the dashboard, uses the default).
|
// docking it in the dashboard, uses the default).
|
||||||
const setValue = (f, v, why) => {
|
const setValue = (f, v, why) => {
|
||||||
const c = curvatureOf(f);
|
const c = curvatureOf(f);
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
@@ -177,13 +253,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ---- UI ------------------------------------------------------------------------------
|
// ---- UI ------------------------------------------------------------------------------
|
||||||
// The whole Toggle Curvature row is the control; the value is shown on its
|
// Menu row: the value is shown on its right with small arrows above/below
|
||||||
// right with small arrows above/below (only where the value can go).
|
// (only where the value can go). Bar button: no indicator.
|
||||||
const STYLE_ID = 'sfui-window-curvature-style';
|
const STYLE_ID = 'sfui-window-curvature-style';
|
||||||
const ROW = `.${CLS.row}`;
|
const ROW = `.${CLS.row}`;
|
||||||
const CSS = `
|
const CSS = `
|
||||||
${ROW}:has(> .sfui-curv-ind) { cursor: ns-resize; }
|
.sfui-curv-ctl { cursor: ns-resize; }
|
||||||
${ROW}:has(> .sfui-curv-ind.sfui-dragging) { background: var(--gamepadui-grey) !important; }
|
.sfui-curv-ctl.sfui-dragging { background: var(--gamepadui-grey, #67707b) !important; }
|
||||||
.sfui-curv-ind { flex: none; align-self: stretch; position: relative; width: 3em; min-height: 0; font-size: 0.72em;
|
.sfui-curv-ind { flex: none; align-self: stretch; position: relative; width: 3em; min-height: 0; font-size: 0.72em;
|
||||||
margin: -8px 0 -8px auto; font-variant-numeric: tabular-nums; line-height: 1; pointer-events: none;
|
margin: -8px 0 -8px auto; font-variant-numeric: tabular-nums; line-height: 1; pointer-events: none;
|
||||||
user-select: none; }
|
user-select: none; }
|
||||||
@@ -208,17 +284,21 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The frame's Toggle Curvature action: a toggle (invocation 2) with the
|
// The frame's Toggle Curvature action: a toggle (invocation 2) with the
|
||||||
// curvature icons. Its row is found by position among the menu's rows,
|
// curvature icons. Its menu row is found by position among the menu's
|
||||||
// checked against (else looked up by) the action's label.
|
// rows, checked against (else looked up by) the action's label; its bar
|
||||||
|
// button by the React `control` prop of the bar's buttons.
|
||||||
const isCurvatureAction = (a) => {
|
const isCurvatureAction = (a) => {
|
||||||
const p = a?.partialParams;
|
const p = a?.partialParams;
|
||||||
return p?.invocation === 2 && p?.icon?.enum === ICON_OFF && p?.icon_active?.enum === ICON_ON;
|
return p?.invocation === 2 && p?.icon?.enum === ICON_OFF && p?.icon_active?.enum === ICON_ON;
|
||||||
};
|
};
|
||||||
|
const isCurvatureItem = (i) => i?.type === 2 && isCurvatureAction(actions.GetAction(i.action_id));
|
||||||
|
const inBar = (f) => (f.protoForSteam?.controls?.items_for_bottom_frame_controls ?? []).some(isCurvatureItem);
|
||||||
const panelOf = (fid) => document.getElementById(`legacy-frame-controls-additional-options-${fid}`);
|
const panelOf = (fid) => document.getElementById(`legacy-frame-controls-additional-options-${fid}`);
|
||||||
|
const barOf = (fid) => document.getElementById(`legacy-frame-controls-${fid}`);
|
||||||
const curvatureRow = (f, panel) => {
|
const curvatureRow = (f, panel) => {
|
||||||
const rows = [...panel.querySelectorAll(ROW)];
|
const rows = [...panel.querySelectorAll(ROW)];
|
||||||
const items = (f.controlAdditionalOptionsItems ?? []).filter((i) => i.type === 2);
|
const items = (f.controlAdditionalOptionsItems ?? []).filter((i) => i.type === 2);
|
||||||
const idx = items.findIndex((i) => isCurvatureAction(actions.GetAction(i.action_id)));
|
const idx = items.findIndex(isCurvatureItem);
|
||||||
if (idx < 0) return { why: 'no curvature action' };
|
if (idx < 0) return { why: 'no curvature action' };
|
||||||
const label = actions.GetAction(items[idx].action_id).protoForSteam?.display_name;
|
const label = actions.GetAction(items[idx].action_id).protoForSteam?.display_name;
|
||||||
const text = (r) => r.querySelector(`.${CLS.label}`)?.textContent;
|
const text = (r) => r.querySelector(`.${CLS.label}`)?.textContent;
|
||||||
@@ -226,6 +306,14 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
if (!row || (label && text(row) !== label)) row = rows.find((r) => label && text(r) === label) ?? null;
|
if (!row || (label && text(row) !== label)) row = rows.find((r) => label && text(r) === label) ?? null;
|
||||||
return row ? { row } : { why: `row not found (${rows.length} rows, ${items.length} items)` };
|
return row ? { row } : { why: `row not found (${rows.length} rows, ${items.length} items)` };
|
||||||
};
|
};
|
||||||
|
const barButton = (node) => {
|
||||||
|
for (const b of node.querySelectorAll('.ButtonControl')) {
|
||||||
|
if (b.closest('[id^="legacy-frame-controls-additional-options-"]')) continue;
|
||||||
|
const fiber = find.findFiberUp(b, (x) => x.memoizedProps?.control != null, 12);
|
||||||
|
if (isCurvatureItem(fiber?.memoizedProps.control)) return b;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// Digits have no descenders, so their ink sits above the centre of the line
|
// Digits have no descenders, so their ink sits above the centre of the line
|
||||||
// box that translateY(-50%) centres. The ink box is measured once (canvas
|
// box that translateY(-50%) centres. The ink box is measured once (canvas
|
||||||
@@ -243,6 +331,14 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
const inkCentre = baseline - (m.actualBoundingBoxAscent - m.actualBoundingBoxDescent) / 2;
|
const inkCentre = baseline - (m.actualBoundingBoxAscent - m.actualBoundingBoxDescent) / 2;
|
||||||
return lh / 2 - inkCentre; // > 0: move down
|
return lh / 2 - inkCentre; // > 0: move down
|
||||||
};
|
};
|
||||||
|
const buildInd = () => {
|
||||||
|
const ind = document.createElement('div');
|
||||||
|
ind.className = 'sfui-curv-ind';
|
||||||
|
for (const c of ['sfui-curv-up', 'sfui-curv-v', 'sfui-curv-dn']) {
|
||||||
|
const d = document.createElement('div'); d.className = c; ind.appendChild(d);
|
||||||
|
}
|
||||||
|
return ind;
|
||||||
|
};
|
||||||
const renderInd = (ind, v) => {
|
const renderInd = (ind, v) => {
|
||||||
const [up, num, dn] = ind.children;
|
const [up, num, dn] = ind.children;
|
||||||
up.textContent = v < o.max ? '▲' : '';
|
up.textContent = v < o.max ? '▲' : '';
|
||||||
@@ -252,118 +348,199 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
num.style.setProperty('--sfui-curv-dy', `${inkShift(num).toFixed(2)}px`);
|
num.style.setProperty('--sfui-curv-dy', `${inkShift(num).toFixed(2)}px`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const controls = new Map(); // frameID -> { ind, row, panel, mo, attach, detach, endDrag }
|
// `${frameID}:menu` / `${frameID}:bar` -> { key, fid, where, panel, el, ind, mo, down }
|
||||||
|
const controls = new Map();
|
||||||
function updateUI(f) {
|
function updateUI(f) {
|
||||||
const c = controls.get(f.frameID);
|
const c = controls.get(`${f.frameID}:menu`);
|
||||||
if (c?.ind.isConnected) renderInd(c.ind, shown(f));
|
if (c?.ind?.isConnected) renderInd(c.ind, shown(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Row control of one frame. Every press/click on the row is stopped at the
|
// ---- input -----------------------------------------------------------------------------
|
||||||
// row, so React (listening at the root) never runs the stock onClick; the
|
// Every press/click on a control is stopped at the element, so React
|
||||||
// click is re-implemented on mouseup when the press never became a drag.
|
// (listening at the root) never runs the stock onClick; the click is
|
||||||
// Drag: vertical and relative (the cylinder bends horizontally, so the
|
// re-implemented on mouseup when the press never became a drag. Drag:
|
||||||
// pointer's y on the panel barely moves while the curvature changes). A
|
// vertical and relative (the cylinder bends horizontally, so the pointer's
|
||||||
// press becomes a drag after dragThreshold px (then re-based, so there is no
|
// y on the panel barely moves while the curvature changes). A press
|
||||||
// jump); the value follows at dragPixelsPerUnit px per 1.0 (the laser's
|
// becomes a drag after dragThreshold px (then re-based, so there is no
|
||||||
// position stops at the menu panel's edge, so the whole range has to fit
|
// jump); the value follows at dragPixelsPerUnit (bar: barDragPixelsPerUnit)
|
||||||
// into the room above/below the row), see dragValue.
|
// px per 1.0, see dragValue. One press at a time, window capture listeners
|
||||||
|
// while it lasts.
|
||||||
const stop = (e) => e.stopPropagation();
|
const stop = (e) => e.stopPropagation();
|
||||||
const STOPPED = ['click', 'dblclick', 'mouseup', 'pointerdown', 'pointerup', 'contextmenu'];
|
const STOPPED = ['click', 'dblclick', 'mouseup', 'pointerdown', 'pointerup', 'contextmenu'];
|
||||||
const buildControl = (fid) => {
|
let press = null; // { c, y0, v0, last, moved, shift, stale }
|
||||||
const frame = () => FS.GetFrame(fid);
|
|
||||||
const ind = document.createElement('div');
|
const emit = (c, type) => {
|
||||||
ind.className = 'sfui-curv-ind';
|
try {
|
||||||
for (const c of ['sfui-curv-up', 'sfui-curv-v', 'sfui-curv-dn']) {
|
c.el?.dispatchEvent(new CustomEvent(type, { bubbles: true, detail: { frameID: c.fid, where: c.where } }));
|
||||||
const d = document.createElement('div'); d.className = c; ind.appendChild(d);
|
} 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();
|
||||||
|
};
|
||||||
|
|
||||||
let drag = null; // { y0, v0, moved }
|
|
||||||
const onMove = (e) => {
|
const onMove = (e) => {
|
||||||
const d = drag, f = frame();
|
const p = press;
|
||||||
if (!d || !f) return;
|
if (!p) return;
|
||||||
if (!d.moved) {
|
const f = FS.GetFrame(p.c.fid);
|
||||||
if (Math.abs(e.clientY - d.y0) < o.dragThreshold) return;
|
if (!f) { endPress('frame gone'); return; }
|
||||||
d.moved = true; d.y0 = e.clientY;
|
let y = e.clientY;
|
||||||
ind.classList.add('sfui-dragging');
|
// 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;
|
||||||
|
}
|
||||||
|
p.last = y;
|
||||||
|
if (!p.moved) {
|
||||||
|
if (Math.abs(y - p.y0) < o.dragThreshold) return;
|
||||||
|
p.moved = true;
|
||||||
|
p.c.el?.classList.add('sfui-dragging');
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const v = dragValue(d.v0 + (d.y0 - e.clientY) / o.dragPixelsPerUnit, shown(f));
|
const ppu = p.c.where === 'bar' ? o.barDragPixelsPerUnit : o.dragPixelsPerUnit;
|
||||||
if (v !== null) setValue(f, v, 'drag');
|
const v = dragValue(p.v0 + (p.y0 - y) / ppu, shown(f));
|
||||||
|
if (v === null) return;
|
||||||
|
setValue(f, v, 'drag');
|
||||||
|
hapticFor(v);
|
||||||
};
|
};
|
||||||
const onUp = (e) => {
|
const onUp = (e) => {
|
||||||
const d = drag;
|
if (e.button === 0) endPress('release');
|
||||||
if (!d) return;
|
};
|
||||||
drag = null;
|
// Ends the press; `release` of a press that never moved is a click.
|
||||||
|
function endPress(why) {
|
||||||
|
const p = press;
|
||||||
|
if (!p) return false;
|
||||||
|
press = null;
|
||||||
window.removeEventListener('mousemove', onMove, true);
|
window.removeEventListener('mousemove', onMove, true);
|
||||||
window.removeEventListener('mouseup', onUp, true);
|
window.removeEventListener('mouseup', onUp, true);
|
||||||
ind.classList.remove('sfui-dragging');
|
p.c.el?.classList.remove('sfui-dragging');
|
||||||
const f = frame();
|
removeRoom(p.c.fid);
|
||||||
if (!d.moved && f && e) setValue(f, curved(f) ? 0 : 1, 'click'); // curved -> flat, flat -> stock
|
if (p.moved) emit(p.c, 'sfui-curv-dragend');
|
||||||
};
|
const f = FS.GetFrame(p.c.fid);
|
||||||
const onDown = (e) => {
|
if (why === 'release') {
|
||||||
|
if (!p.moved && f) setValue(f, curved(f) ? 0 : 1, `click (${p.c.where})`); // curved -> flat, flat -> stock
|
||||||
|
} else {
|
||||||
|
log('press ended', { frame: p.c.fid, where: p.c.where, why, dragged: p.moved });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const onDown = (c) => (e) => {
|
||||||
e.stopPropagation(); e.preventDefault();
|
e.stopPropagation(); e.preventDefault();
|
||||||
const f = frame();
|
const f = FS.GetFrame(c.fid);
|
||||||
if (e.button !== 0 || drag || !f) return;
|
if (e.button !== 0 || !f) return;
|
||||||
drag = { y0: e.clientY, v0: shown(f), moved: false };
|
endPress('new press');
|
||||||
|
press = { c, y0: e.clientY, last: e.clientY, v0: shown(f), moved: false, shift: 0, stale: false };
|
||||||
window.addEventListener('mousemove', onMove, true);
|
window.addEventListener('mousemove', onMove, true);
|
||||||
window.addEventListener('mouseup', onUp, true);
|
window.addEventListener('mouseup', onUp, true);
|
||||||
};
|
};
|
||||||
const attach = (row) => {
|
|
||||||
row.addEventListener('mousedown', onDown);
|
|
||||||
for (const t of STOPPED) row.addEventListener(t, stop);
|
|
||||||
};
|
|
||||||
const detach = (row) => {
|
|
||||||
row.removeEventListener('mousedown', onDown);
|
|
||||||
for (const t of STOPPED) row.removeEventListener(t, stop);
|
|
||||||
};
|
|
||||||
return { ind, attach, detach, endDrag: () => onUp(null) };
|
|
||||||
};
|
|
||||||
|
|
||||||
const inject = (f) => {
|
// ---- controls --------------------------------------------------------------------------
|
||||||
const fid = f.frameID;
|
const setEl = (c, el) => {
|
||||||
const panel = panelOf(fid);
|
if (c.el === el) return;
|
||||||
|
if (c.el) detach(c);
|
||||||
|
c.el = el;
|
||||||
|
el.classList.add('sfui-curv-ctl');
|
||||||
|
if (press?.c === c && press.moved) el.classList.add('sfui-dragging'); // re-rendered mid-drag
|
||||||
|
el.addEventListener('mousedown', c.down);
|
||||||
|
for (const t of STOPPED) el.addEventListener(t, stop);
|
||||||
|
};
|
||||||
|
function detach(c) {
|
||||||
|
c.el.classList.remove('sfui-curv-ctl', 'sfui-dragging');
|
||||||
|
c.el.removeEventListener('mousedown', c.down);
|
||||||
|
for (const t of STOPPED) c.el.removeEventListener(t, stop);
|
||||||
|
}
|
||||||
|
const newControl = (key, fid, where, panel) => {
|
||||||
|
const c = { key, fid, where, panel, el: null, ind: where === 'menu' ? buildInd() : null };
|
||||||
|
c.down = onDown(c);
|
||||||
|
c.mo = new MutationObserver(() => {
|
||||||
|
if (!c.el?.isConnected || (c.ind && c.ind.parentElement !== c.el)) queueSync();
|
||||||
|
});
|
||||||
|
c.mo.observe(panel, { childList: true, subtree: true });
|
||||||
|
controls.set(key, c);
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
function removeControl(key) {
|
||||||
|
const c = controls.get(key);
|
||||||
|
if (!c) return;
|
||||||
|
if (press?.c === c) endPress('control removed');
|
||||||
|
c.mo.disconnect();
|
||||||
|
if (c.el) detach(c);
|
||||||
|
c.ind?.remove();
|
||||||
|
controls.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const injectMenu = (f) => {
|
||||||
|
const key = `${f.frameID}:menu`, panel = panelOf(f.frameID);
|
||||||
if (!panel) return 'no panel';
|
if (!panel) return 'no panel';
|
||||||
let c = controls.get(fid);
|
let c = controls.get(key);
|
||||||
if (c && c.panel !== panel) { removeControl(fid); c = null; }
|
if (c && c.panel !== panel) { removeControl(key); c = null; }
|
||||||
if (c?.ind.isConnected && c.row?.contains(c.ind)) { renderInd(c.ind, shown(f)); return 'ok'; }
|
if (c?.el?.isConnected && c.ind.parentElement === c.el) { renderInd(c.ind, shown(f)); return 'ok'; }
|
||||||
const r = curvatureRow(f, panel);
|
const r = curvatureRow(f, panel);
|
||||||
if (!r.row) { log('inject failed', { frame: fid, why: r.why }); return r.why; }
|
if (!r.row) return r.why;
|
||||||
ensureStyle();
|
ensureStyle();
|
||||||
if (!c) {
|
c ??= newControl(key, f.frameID, 'menu', panel);
|
||||||
const b = buildControl(fid);
|
setEl(c, r.row);
|
||||||
const mo = new MutationObserver(() => { if (!b.ind.isConnected || !b.ind.parentElement?.matches(ROW)) queueSync(); });
|
|
||||||
mo.observe(panel, { childList: true, subtree: true });
|
|
||||||
c = { ...b, panel, mo, row: null };
|
|
||||||
controls.set(fid, c);
|
|
||||||
}
|
|
||||||
if (c.row !== r.row) {
|
|
||||||
if (c.row) c.detach(c.row);
|
|
||||||
c.row = r.row;
|
|
||||||
c.attach(r.row);
|
|
||||||
}
|
|
||||||
r.row.append(c.ind);
|
r.row.append(c.ind);
|
||||||
renderInd(c.ind, shown(f));
|
renderInd(c.ind, shown(f));
|
||||||
return 'ok';
|
return 'ok';
|
||||||
};
|
};
|
||||||
|
const injectBar = (f) => {
|
||||||
|
const key = `${f.frameID}:bar`, node = barOf(f.frameID);
|
||||||
|
if (!node) return 'no bar panel';
|
||||||
|
let c = controls.get(key);
|
||||||
|
if (c && c.panel !== node) { removeControl(key); c = null; }
|
||||||
|
if (c?.el?.isConnected && node.contains(c.el)) return 'ok';
|
||||||
|
const btn = barButton(node);
|
||||||
|
if (!btn) return 'bar button not found';
|
||||||
|
ensureStyle();
|
||||||
|
c ??= newControl(key, f.frameID, 'bar', node);
|
||||||
|
setEl(c, btn);
|
||||||
|
return 'ok';
|
||||||
|
};
|
||||||
|
|
||||||
function removeControl(fid) {
|
// Wanted controls: open menus, and bars that hold the curvature action;
|
||||||
const c = controls.get(fid);
|
// injected with retries until React has rendered them (bars only while
|
||||||
if (!c) return;
|
// the frame is visible: a hidden frame has no bar panel).
|
||||||
c.mo.disconnect(); c.endDrag();
|
|
||||||
if (c.row) c.detach(c.row);
|
|
||||||
c.ind.remove();
|
|
||||||
controls.delete(fid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open menus: inject (retrying until React has rendered the panel).
|
|
||||||
const openFrames = () => FS.frames.filter((f) => f.isControlAdditionalOptionsOpen);
|
|
||||||
let syncTimer = null, retries = 0;
|
let syncTimer = null, retries = 0;
|
||||||
|
const failed = new Map(); // key -> last reason logged
|
||||||
const sync = () => {
|
const sync = () => {
|
||||||
syncTimer = null;
|
syncTimer = null;
|
||||||
const open = openFrames();
|
const want = new Map();
|
||||||
for (const fid of [...controls.keys()]) if (!open.some((f) => f.frameID === fid)) removeControl(fid);
|
for (const f of FS.frames) {
|
||||||
|
if (f.isControlAdditionalOptionsOpen) want.set(`${f.frameID}:menu`, f);
|
||||||
|
if (inBar(f)) want.set(`${f.frameID}:bar`, f);
|
||||||
|
}
|
||||||
|
for (const key of [...controls.keys()]) if (!want.has(key)) removeControl(key);
|
||||||
|
for (const key of [...failed.keys()]) if (!want.has(key)) failed.delete(key);
|
||||||
let pending = false;
|
let pending = false;
|
||||||
for (const f of open) if (inject(f) !== 'ok') pending = true;
|
for (const [key, f] of want) {
|
||||||
|
const bar = key.endsWith(':bar');
|
||||||
|
const r = bar ? injectBar(f) : injectMenu(f);
|
||||||
|
if (r === 'ok') { failed.delete(key); continue; }
|
||||||
|
if (bar ? !f.isCurrentlyVisible : r === 'no curvature action') continue; // hidden frame / not in this menu
|
||||||
|
pending = true;
|
||||||
|
if (failed.get(key) !== r) { failed.set(key, r); log('inject failed', { frame: f.frameID, where: bar ? 'bar' : 'menu', why: r }); }
|
||||||
|
}
|
||||||
if (pending && retries < 12) { retries++; syncTimer = setTimeout(sync, 60 * retries); } else retries = 0;
|
if (pending && retries < 12) { retries++; syncTimer = setTimeout(sync, 60 * retries); } else retries = 0;
|
||||||
};
|
};
|
||||||
function queueSync() { if (!syncTimer) syncTimer = setTimeout(sync, 0); }
|
function queueSync() { if (!syncTimer) syncTimer = setTimeout(sync, 0); }
|
||||||
@@ -371,16 +548,19 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
hooks.before(proto, 'SendMessage', NAME, hook);
|
hooks.before(proto, 'SendMessage', NAME, hook);
|
||||||
const disposers = [
|
const disposers = [
|
||||||
mx.reaction(
|
mx.reaction(
|
||||||
() => FS.frames.map((f) => `${f.frameID}:${f.isControlAdditionalOptionsOpen ? 1 : 0}:${curved(f) ? 1 : 0}:${dockOf(f)}`).join(','),
|
() => FS.frames.map((f) => [f.frameID, f.isControlAdditionalOptionsOpen ? 1 : 0, curved(f) ? 1 : 0, dockOf(f),
|
||||||
|
inBar(f) ? 1 : 0, f.isCurrentlyVisible ? 1 : 0].join(':')).join(','),
|
||||||
() => { retries = 0; queueSync(); for (const f of FS.frames) updateUI(f); },
|
() => { retries = 0; queueSync(); for (const f of FS.frames) updateUI(f); },
|
||||||
),
|
),
|
||||||
mx.reaction(() => DS.curvatureDistance, () => resend()),
|
mx.reaction(() => DS.curvatureDistance, () => resend()),
|
||||||
];
|
];
|
||||||
|
|
||||||
const teardown = () => {
|
const teardown = () => {
|
||||||
|
endPress('teardown');
|
||||||
for (const d of disposers.splice(0)) d();
|
for (const d of disposers.splice(0)) d();
|
||||||
clearTimeout(syncTimer); clearTimeout(resendTimer);
|
clearTimeout(syncTimer); clearTimeout(resendTimer);
|
||||||
for (const fid of [...controls.keys()]) removeControl(fid);
|
for (const key of [...controls.keys()]) removeControl(key);
|
||||||
|
for (const fid of [...rooms.keys()]) removeRoom(fid);
|
||||||
document.getElementById(STYLE_ID)?.remove();
|
document.getElementById(STYLE_ID)?.remove();
|
||||||
hooks.remove(proto, 'SendMessage', NAME);
|
hooks.remove(proto, 'SendMessage', NAME);
|
||||||
if (window.__sfuiWindowCurvature === state) delete window.__sfuiWindowCurvature;
|
if (window.__sfuiWindowCurvature === state) delete window.__sfuiWindowCurvature;
|
||||||
@@ -388,7 +568,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
};
|
};
|
||||||
const check = () => {
|
const check = () => {
|
||||||
if (!hooks.has(proto, 'SendMessage', NAME)) hooks.before(proto, 'SendMessage', NAME, hook);
|
if (!hooks.has(proto, 'SendMessage', NAME)) hooks.before(proto, 'SendMessage', NAME, hook);
|
||||||
if (openFrames().length) queueSync();
|
queueSync();
|
||||||
return 'unchanged';
|
return 'unchanged';
|
||||||
};
|
};
|
||||||
const dump = () => FS.frames.map((f) => ({
|
const dump = () => FS.frames.map((f) => ({
|
||||||
@@ -396,15 +576,21 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
override: curvatureOf(f)?.m_bCurveOverride, stored: S.values[keyOf(f)], shown: shown(f),
|
override: curvatureOf(f)?.m_bCurveOverride, stored: S.values[keyOf(f)], shown: shown(f),
|
||||||
stockDistance: curvatureOf(f)?.curvatureOriginDistance,
|
stockDistance: curvatureOf(f)?.curvatureOriginDistance,
|
||||||
appliedDistance: curved(f) ? DS.curvatureDistance / strength(f) : null,
|
appliedDistance: curved(f) ? DS.curvatureDistance / strength(f) : null,
|
||||||
menuOpen: f.isControlAdditionalOptionsOpen, control: !!controls.get(f.frameID)?.ind.isConnected,
|
menuOpen: f.isControlAdditionalOptionsOpen, inBar: inBar(f), visible: f.isCurrentlyVisible,
|
||||||
|
control: !!controls.get(`${f.frameID}:menu`)?.el?.isConnected,
|
||||||
|
barControl: !!controls.get(`${f.frameID}:bar`)?.el?.isConnected,
|
||||||
}));
|
}));
|
||||||
const state = {
|
const state = {
|
||||||
version: VERSION, id, opts: o, hits, log: logBuf, state: S, teardown, check, dump, dragValue,
|
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'; },
|
setValue: (fid, v) => { const f = FS.GetFrame(fid); if (f) setValue(f, v, 'api'); return f ? shown(f) : 'no frame'; },
|
||||||
|
cancelPress: () => endPress('cancelled'),
|
||||||
|
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 }; },
|
||||||
};
|
};
|
||||||
window.__sfuiWindowCurvature = state;
|
window.__sfuiWindowCurvature = state;
|
||||||
log('patched', { version: VERSION, opts: o });
|
log('patched', { version: VERSION, opts: o });
|
||||||
resend();
|
resend();
|
||||||
queueSync();
|
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.snap} at ${o.snapPoints.join('/')}, ` +
|
||||||
|
`haptics ${o.haptics ? 'on' : 'off'})`;
|
||||||
})
|
})
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
// Reverts window-curvature/patch.js: removes the control from open menus,
|
// Reverts window-curvature/patch.js: ends a running press or drag, removes
|
||||||
// its style, reactions and SendMessage hook (the shared wrapper goes too once
|
// the control from open menus and bar buttons (class, listeners, value
|
||||||
// no other patch uses it) and resends the scene graph, so windows get the
|
// indicator, a bar's drag room), its style, reactions and SendMessage hook
|
||||||
// stock curve radius back. Curvature on/off is stock state and stays as it
|
// (the shared wrapper goes too once no other patch uses it) and resends the
|
||||||
// is. window.__sfuiWindowCurvatureState (per-window values) is kept for a
|
// scene graph, so windows get the stock curve radius back. Curvature on/off
|
||||||
// re-injection. Safe when not patched.
|
// is stock state and stays as it is. window.__sfuiWindowCurvatureState
|
||||||
|
// (per-window values) is kept for a re-injection. Safe when not patched.
|
||||||
(() => {
|
(() => {
|
||||||
const s = window.__sfuiWindowCurvature;
|
const s = window.__sfuiWindowCurvature;
|
||||||
if (!s) return 'not patched';
|
if (!s) return 'not patched';
|
||||||
|
|||||||
+2
-1
@@ -43,7 +43,8 @@
|
|||||||
# # just the dashboard bar:
|
# # just the dashboard bar:
|
||||||
# steamCloseButton.enable = true;
|
# steamCloseButton.enable = true;
|
||||||
# # Curvature per window: click the "Toggle Curvature" row of a
|
# # Curvature per window: click the "Toggle Curvature" row of a
|
||||||
# # window's More Options menu to toggle, drag up/down to adjust:
|
# # window's More Options menu (or its bar button) to toggle, drag
|
||||||
|
# # up/down to adjust (with controller haptics):
|
||||||
# windowCurvature.enable = true;
|
# windowCurvature.enable = true;
|
||||||
# };
|
# };
|
||||||
# firefox.enable = true; # launcher for the Firefox Flatpak
|
# firefox.enable = true; # launcher for the Firefox Flatpak
|
||||||
|
|||||||
Reference in new issue
Block a user