diff --git a/docs/architecture/07-cursor.md b/docs/architecture/07-cursor.md
index e34096d7..a32ed98a 100644
--- a/docs/architecture/07-cursor.md
+++ b/docs/architecture/07-cursor.md
@@ -131,10 +131,10 @@ slightly during zoom ramps, nearest keeps it pixelated and steady. Pure rules:
session-end reset of `ixPubLevel_` keeps a quick zoom back to the same level from skipping the
publish nudge, and the hide-cursor hotkey's show-again transition nudges after the blanker
restore like the zoom-out does.
-- **Pan glide (#430, `panGlideMaxPx`, experimental, 0 = off).** A soft stop: when a mouse movement
+- **Pan glide (#430, `panGlideMaxPx`, 0 = off).** A soft stop: when a mouse movement
stops, the POINTER eases on at the hand's speed and slows to rest within `panGlideMaxPx` SCREEN px
- at any zoom (`src/pan_glide.h`); `panGlideMs` is the ease's time constant, shortened for a fast hand
- so the cap holds. The view follows by DWM centring. Any hand movement, a mouse button, a game,
+ at any zoom (`src/pan_glide.h`). One setting since #434: the ease's time constant follows the
+ distance (`PanGlideTauS`, 40 px = 60 ms), shortened for a fast hand so the cap holds. The view follows by DWM centring. Any hand movement, a mouse button, a game,
Inspect, a detached view or 1x ends it. Wind's own steps move the tick baseline (`lastSetVirtual`),
so the lock detector and the gain learner only ever see hand motion. A first, uncapped version
(momentum = speed x time) could throw the pointer across the screen and was replaced. It replaces
diff --git a/docs/architecture/08-config-profiles.md b/docs/architecture/08-config-profiles.md
index 9e0bf3ca..023334bb 100644
--- a/docs/architecture/08-config-profiles.md
+++ b/docs/architecture/08-config-profiles.md
@@ -173,7 +173,7 @@ Every key works in the ini whether or not Settings shows it. Keys hot-reload unl
| Keys | Meaning |
|---|---|
| `maxLevel` (12), `zoomInSpeed`, `zoomOutSpeed`, `zoomEaseOutMs`, `smoothZoom*` | Range and feel of the zoom |
-| `cursorSensitivity` (1.0), `panSpeed` (1.0), `panGlideMaxPx` (0), `panGlideMs` (60) | Mouse and arrow-key pan speed; the pan glide's distance cap and ease (#430) |
+| `cursorSensitivity` (1.0), `panSpeed` (1.0), `panGlideMaxPx` (0) | Mouse and arrow-key pan speed; the pan glide's distance, which also sets its ease (#430, #434) |
| `mouseAlign` (0 centred, 1 within the edges), `mouseMarginPct` | Where the pointer sits while the view moves |
| `trackCaret` (1), `trackFocus` (0), `trackAlign`, `trackGlideMs` (200) | Follow the text caret and keyboard focus |
| `lockApps`, `warpLock` (0) | Games whose sessions pan from raw mouse motion; heuristics for unlisted games |
diff --git a/src/config.cpp b/src/config.cpp
index 0442eef2..923132b3 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -210,7 +210,6 @@ Config ParseConfig(const std::string& text) {
else if (key == "dwmFlush") c.dwmFlush = ParseIntStrict(val);
else if (key == "diagnostics") c.diagnostics = ParseIntStrict(val);
else if (key == "cursorSensitivity") c.cursorSensitivity = ParseDoubleStrict(val);
- else if (key == "panGlideMs") c.panGlideMs = ParseIntStrict(val);
else if (key == "panGlideMaxPx") c.panGlideMaxPx = ParseIntStrict(val);
else if (key == "cursorConstantSize") c.cursorConstantSize = ParseIntStrict(val);
else if (key == "cursorVisibility") c.cursorVisibility = val;
@@ -296,8 +295,6 @@ Config ParseConfig(const std::string& text) {
c.smoothZoomAccel = clampd(c.smoothZoomAccel, 1.0, 8.0);
c.smoothZoomRamp = clampd(c.smoothZoomRamp, 0.1, 3.0);
c.cursorSensitivity = clampd(c.cursorSensitivity, 0.25, 4.0);
- if (c.panGlideMs < 10) c.panGlideMs = 10;
- if (c.panGlideMs > 300) c.panGlideMs = 300;
if (c.panGlideMaxPx < 0) c.panGlideMaxPx = 0;
if (c.panGlideMaxPx > 400) c.panGlideMaxPx = 400;
c.sharpness = clampd(c.sharpness, 0.0, 1.0);
@@ -518,9 +515,8 @@ std::string DefaultIniText() {
"; No effect in a free transform session (the pointer itself drives the view)\n"
"cursorSensitivity=1.0\n"
"; panGlideMaxPx: when a mouse movement stops the pointer eases on at most this many screen\n"
- "; px before it rests (Transform engine, zoomed); 0=off. panGlideMs: the ease's time (ms)\n"
+ "; px before it rests (Transform engine, zoomed); 0=off. A longer glide also eases longer\n"
"panGlideMaxPx=0\n"
- "panGlideMs=60\n"
"; cursorConstantSize: 0=the cursor grows with the zoom (default); 1=keep it at normal\n"
"; desktop size at every zoom (render engine only). Replaces cursorScaleWithZoom (ignored).\n"
"cursorConstantSize=0\n"
diff --git a/src/config.h b/src/config.h
index 6a74f63b..1eff2de3 100644
--- a/src/config.h
+++ b/src/config.h
@@ -380,10 +380,9 @@ struct Config {
double cursorSensitivity = 1.0;
// Pan glide (issue #430, src/pan_glide.h): when a mouse movement stops, the pointer eases on and
// comes to rest instead of halting dead. panGlideMaxPx caps how far, in SCREEN px at any zoom
- // (0 = off, the default); panGlideMs is the ease's time constant (a fast hand eases more steeply
- // to stay inside the cap). Hot. Replaces cursorSmoothing (ignored).
+ // (0 = off, the default). The ease's time follows the distance (PanGlideTauS, one setting since
+ // #434; the old panGlideMs key is ignored). Hot. Replaces cursorSmoothing (ignored).
int panGlideMaxPx = 0;
- int panGlideMs = 60;
// 0 (default) = the render engine's cursor grows with the zoom, matching the transform
// engine (DWM magnifies its sprite); 1 = opt-in constant desktop-size pointer. Replaces
// cursorScaleWithZoom (issue #253), which is IGNORED: the default template wrote it as an
diff --git a/src/main.cpp b/src/main.cpp
index ff3a6dac..8a4c88ec 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1869,7 +1869,7 @@ static void RunTick(TickState& t) {
!t.viewDetached && !btn;
if (t.glide.active && (!glideOk || curDx != 0 || curDy != 0)) wind::PanGlideCancel(t.glide);
if (!t.glide.active)
- wind::PanGlideObserve(t.glide, curDx, curDy, dt, cur.x, cur.y, t.cfg.panGlideMs / 1000.0,
+ wind::PanGlideObserve(t.glide, curDx, curDy, dt, cur.x, cur.y, wind::PanGlideTauS(t.cfg.panGlideMaxPx),
t.cfg.panGlideMaxPx / lvl, glideOk); // the cap is in screen px
double gx = 0.0, gy = 0.0;
if (t.glide.active && wind::PanGlideStep(t.glide, dt, gx, gy)) {
diff --git a/src/pan_glide.h b/src/pan_glide.h
index f6379735..499f2b79 100644
--- a/src/pan_glide.h
+++ b/src/pan_glide.h
@@ -29,6 +29,14 @@ inline constexpr double kGlideVelTauS = 0.03; // velocity estimate: the
inline constexpr double kGlideMinStartSpeed = 40.0; // px/s: below this the hand was resting anyway
inline constexpr double kGlideStopDist = 0.5; // desktop px left to travel: the glide is over
+// One setting drives the glide (#434): its ease time grows with its distance, so a longer glide also
+// slows down more gently. 40 px gives 60 ms, the old separate default.
+inline double PanGlideTauS(int maxPx) {
+ double ms = 40.0 + maxPx * 0.5;
+ if (ms > 240.0) ms = 240.0;
+ return ms / 1000.0;
+}
+
// One tick of hand motion (dx, dy desktop px over dt s), with the pointer now at (curX, curY).
// Starts a glide (returns true) on the first still tick after motion when the session allows it.
// tauS is the ease's time constant; maxDist (desktop px, > 0) caps how far it travels: a hand fast
diff --git a/src/version.h b/src/version.h
index fa8f0731..c33e7b8b 100644
--- a/src/version.h
+++ b/src/version.h
@@ -4,7 +4,7 @@
#define WIND_VER_MAJOR 0
#define WIND_VER_MINOR 26
-#define WIND_VER_PATCH 1
+#define WIND_VER_PATCH 2
// String form for logs/snapshot/UI. Keep in sync with the numeric parts above.
-#define WIND_VERSION_STR "0.26.1"
+#define WIND_VERSION_STR "0.26.2"
diff --git a/tests/test_config.cpp b/tests/test_config.cpp
index f944bb3f..08a610b7 100644
--- a/tests/test_config.cpp
+++ b/tests/test_config.cpp
@@ -58,7 +58,6 @@ TEST_CASE("renderer knobs have sane defaults") {
CHECK(c.bilinear == 1);
CHECK(c.sharpness == doctest::Approx(0.0)); // off by default
CHECK(c.panGlideMaxPx == 0);
- CHECK(c.panGlideMs == 60);
CHECK(c.zorderBand == 0); // shipped unbanded (issue #162): band 16 sat UNDER the
// Snipping Tool overlay, which cost the cursor entirely
CHECK(c.brightness == doctest::Approx(1.0));
@@ -140,9 +139,9 @@ TEST_CASE("hdrTonemap can be disabled") {
Config c = ParseConfig("hdrTonemap=0\n");
CHECK(c.hdrTonemap == 0);
}
-TEST_CASE("parses panGlideMs, z-order band, brightness; the old cursorSmoothing is ignored") {
+TEST_CASE("parses z-order band, brightness; the old cursorSmoothing and panGlideMs are ignored") {
Config c = ParseConfig("panGlideMs=150\ncursorSmoothing=0.7\nzorderBand=16\nbrightness=0.85\n");
- CHECK(c.panGlideMs == 150);
+ CHECK(c.panGlideMaxPx == 0);
CHECK(c.zorderBand == 16);
CHECK(c.brightness == doctest::Approx(0.85));
}
@@ -178,14 +177,12 @@ TEST_CASE("numeric fields are clamped to documented ranges") {
CHECK(ParseConfig("smoothZoomAccel=0\n").smoothZoomAccel == doctest::Approx(1.0));
CHECK(ParseConfig("smoothZoomRamp=-1\n").smoothZoomRamp == doctest::Approx(0.1));
CHECK(ParseConfig("cursorSensitivity=10\n").cursorSensitivity == doctest::Approx(4.0));
- CHECK(ParseConfig("panGlideMs=5000\n").panGlideMs == 300);
- CHECK(ParseConfig("panGlideMs=-1\n").panGlideMs == 10);
CHECK(ParseConfig("panGlideMaxPx=9999\n").panGlideMaxPx == 400);
CHECK(ParseConfig("sharpness=9\n").sharpness == doctest::Approx(1.0));
CHECK(ParseConfig("brightness=0\n").brightness == doctest::Approx(0.5));
// In-range values pass through untouched.
CHECK(ParseConfig("maxLevel=8\n").maxLevel == doctest::Approx(8.0));
- CHECK(ParseConfig("panGlideMs=150\n").panGlideMs == 150);
+ CHECK(ParseConfig("panGlideMaxPx=40\n").panGlideMaxPx == 40);
}
TEST_CASE("multiMonitor can be set") {
CHECK(ParseConfig("multiMonitor=0\n").multiMonitor == 0);
@@ -460,7 +457,6 @@ TEST_CASE("the first-run ini template parses to the struct defaults (issue #274)
CHECK(t.dwmFlush == d.dwmFlush);
CHECK(t.diagnostics == d.diagnostics);
CHECK(t.cursorSensitivity == doctest::Approx(d.cursorSensitivity));
- CHECK(t.panGlideMs == d.panGlideMs);
CHECK(t.panGlideMaxPx == d.panGlideMaxPx);
CHECK(t.cursorConstantSize == d.cursorConstantSize);
CHECK(t.cursorVisibility == d.cursorVisibility);
diff --git a/tests/test_pan_glide.cpp b/tests/test_pan_glide.cpp
index 4480cb29..231eb2de 100644
--- a/tests/test_pan_glide.cpp
+++ b/tests/test_pan_glide.cpp
@@ -37,6 +37,14 @@ TEST_CASE("the cap bounds the distance at any speed and keeps the hand's start s
}
}
+TEST_CASE("one setting: the ease time grows with the distance (#434)") {
+ CHECK(PanGlideTauS(40) == doctest::Approx(0.060)); // the old separate default
+ CHECK(PanGlideTauS(0) == doctest::Approx(0.040));
+ CHECK(PanGlideTauS(200) == doctest::Approx(0.140));
+ CHECK(PanGlideTauS(400) == doctest::Approx(0.240));
+ CHECK(PanGlideTauS(1000) == doctest::Approx(0.240));
+}
+
TEST_CASE("no glide when it is off or the session does not allow it") {
PanGlide off;
CHECK_FALSE(Flick(off, 20, 20.0, 0.0, 0.0)); // panGlideMs = 0
diff --git a/ui/src/Onboarding.svelte b/ui/src/Onboarding.svelte
index 82457b4f..a72413c7 100644
--- a/ui/src/Onboarding.svelte
+++ b/ui/src/Onboarding.svelte
@@ -32,8 +32,8 @@
}
function back() { if (cur > 0) cur -= 1; }
function skip() { setConfig('onboarded', '1'); onDone(); }
- const zoomInRow = { label:'Zoom in', desc:'Hold to magnify', buttonKey:'zoomInButton', vkKey:'zoomInVk', modsKey:'zoomInMods', buttonModsKey:'zoomInButtonMods' };
- const zoomOutRow = { label:'Zoom out', desc:'Hold to zoom back', buttonKey:'zoomOutButton', vkKey:'zoomOutVk', modsKey:'zoomOutMods', buttonModsKey:'zoomOutButtonMods' };
+ const zoomInRow = { label:'Zoom in', desc:'Hold to zoom in', buttonKey:'zoomInButton', vkKey:'zoomInVk', modsKey:'zoomInMods', buttonModsKey:'zoomInButtonMods' };
+ const zoomOutRow = { label:'Zoom out', desc:'Hold to zoom out', buttonKey:'zoomOutButton', vkKey:'zoomOutVk', modsKey:'zoomOutMods', buttonModsKey:'zoomOutButtonMods' };
@@ -68,16 +68,16 @@
Welcome to Wind
-
Barely there. Everywhere. Let's set up the essentials.
+
A few quick steps to get started
Set your zoom keys
-
Pick the buttons you'll hold to zoom. Mouse side-buttons work great, or choose keyboard keys.
-
+
Pick the buttons or keys you hold to zoom
+
-
Zoom out
Hold to zoom back
+
diff --git a/ui/src/Settings.svelte b/ui/src/Settings.svelte
index 3b92467b..01be5fef 100644
--- a/ui/src/Settings.svelte
+++ b/ui/src/Settings.svelte
@@ -369,15 +369,23 @@
diff --git a/ui/src/shell/Card.svelte b/ui/src/shell/Card.svelte
index c3226ec6..6babb117 100644
--- a/ui/src/shell/Card.svelte
+++ b/ui/src/shell/Card.svelte
@@ -9,6 +9,6 @@
diff --git a/ui/src/shell/SaveCapsule.svelte b/ui/src/shell/SaveCapsule.svelte
index a27d8c20..e294f185 100644
--- a/ui/src/shell/SaveCapsule.svelte
+++ b/ui/src/shell/SaveCapsule.svelte
@@ -43,8 +43,8 @@
{/if}
diff --git a/ui/src/tray/TrayMenuPage.svelte b/ui/src/tray/TrayMenuPage.svelte
index abcef2e0..ee622bbd 100644
--- a/ui/src/tray/TrayMenuPage.svelte
+++ b/ui/src/tray/TrayMenuPage.svelte
@@ -25,7 +25,7 @@
const blocked = (kind, it) => kind === 'sliders' && !it.on && full;
-
Checked items appear in the tray. Drag to reorder.
+
Check items to show them and drag to reorder
{#snippet itemRow(kind, it)}
{@html iconSvg(it.icon, 24)}
@@ -41,7 +41,7 @@
Sliders
- {sliderCount} of {MAX_SLIDERS}{full ? ' · Uncheck one to add another' : ''}
+ {sliderCount} of {MAX_SLIDERS}{full ? ' full' : ''}
flip('sliders', sliders, it)}
diff --git a/ui/src/tray/trayModel.js b/ui/src/tray/trayModel.js
index d5a9925e..becc0cfe 100644
--- a/ui/src/tray/trayModel.js
+++ b/ui/src/tray/trayModel.js
@@ -7,22 +7,22 @@ export const MAX_SLIDERS = 4;
// `icon` names live in design/icons.js. Order here is the default order.
export const SLIDERS = [
- { key: 'colorWarmPct', icon: 'warm', name: 'Warmth', desc: 'Screen warmth.' },
- { key: 'colorDimPct', icon: 'bright', name: 'Brightness', desc: 'Screen brightness.' },
- { key: 'maxLevel', icon: 'maxz', name: 'Max zoom', desc: 'The highest magnification.' },
- { key: 'zoomInSpeed', icon: 'zin', name: 'Zoom-in speed', desc: 'How quickly the view magnifies.' },
- { key: 'zoomOutSpeed', icon: 'zout', name: 'Zoom-out speed', desc: 'How quickly the view returns.' },
- { key: 'panSpeed', icon: 'pan', name: 'Arrow key speed', desc: 'How fast the view pans.' },
- { key: 'panGlideMaxPx', icon: 'smooth', name: 'Pan glide', desc: 'How far the pointer eases on when you stop.' },
- { key: 'zoomEaseOutMs', icon: 'glide', name: 'Release glide', desc: 'How softly the zoom coasts to a stop.' },
+ { key: 'colorWarmPct', icon: 'warm', name: 'Warmth', desc: 'Screen warmth' },
+ { key: 'colorDimPct', icon: 'bright', name: 'Brightness', desc: 'Screen brightness' },
+ { key: 'maxLevel', icon: 'maxz', name: 'Max zoom', desc: 'The highest zoom level' },
+ { key: 'zoomInSpeed', icon: 'zin', name: 'Zoom-in speed', desc: 'How fast the view zooms in' },
+ { key: 'zoomOutSpeed', icon: 'zout', name: 'Zoom-out speed', desc: 'How fast the view zooms out' },
+ { key: 'panSpeed', icon: 'pan', name: 'Arrow key speed', desc: 'How fast the view pans' },
+ { key: 'panGlideMaxPx', icon: 'smooth', name: 'Pan glide', desc: 'How far the pointer coasts after you stop' },
+ { key: 'zoomEaseOutMs', icon: 'glide', name: 'Release glide', desc: 'How softly the zoom coasts to a stop' },
];
export const TOGGLES = [
- { key: 'trackCaret', icon: 'ftc', name: 'Follow the text cursor', desc: 'Follow the caret while you type.' },
- { key: 'trackFocus', icon: 'ffk', name: 'Follow keyboard focus', desc: 'Follow keyboard focus around the screen.' },
+ { key: 'trackCaret', icon: 'ftc', name: 'Follow the text cursor', desc: 'The view follows the text cursor' },
+ { key: 'trackFocus', icon: 'ffk', name: 'Follow keyboard focus', desc: 'The view follows keyboard focus' },
// One segment for both alignment settings (mouseAlign + trackAlign); the flyout writes both.
- { key: 'keepEdges', icon: 'edges', name: 'Keep cursor centred', desc: 'On: the view keeps the pointer, text cursor and focus centred. Off: they move freely and the view follows near the edges.' },
+ { key: 'keepEdges', icon: 'edges', name: 'Keep cursor centred', desc: 'Keeps the pointer and text cursor centred' },
// #315. A full-width dropdown under the toggle group in the tray that picks the main engine; a change restarts Wind.
- { key: 'engine', icon: 'engine', name: 'Engine', desc: 'A dropdown to switch the engine. Wind restarts to apply it.' },
+ { key: 'engine', icon: 'engine', name: 'Engine', desc: 'Switch the engine from the tray' },
];
const KINDS = {
sliders: { items: SLIDERS, enabledKey: 'traySliders', orderKey: 'traySliderOrder', defaultOn: ['colorWarmPct', 'colorDimPct'], cap: MAX_SLIDERS },
diff --git a/ui/tests/prefs.spec.js b/ui/tests/prefs.spec.js
index 917f2d09..2816b6de 100644
--- a/ui/tests/prefs.spec.js
+++ b/ui/tests/prefs.spec.js
@@ -100,10 +100,10 @@ test('the theme picker is one row of the four theme cards, no scrolling, no arro
expect(await css(picker, 'overflow-x')).toBe('visible');
await expect(key(page, 'uiPalette').locator('button')).toHaveCount(4); // the four cards only
expect(await css(picker, 'mask-image')).toBe('none');
- // Right-aligned like the other controls: the last card ends at the row's right edge.
+ // At this width the row stacks (control under the text): the last card stays inside the row.
const rowBox = await key(page, 'uiPalette').boundingBox();
const lastBox = await picker.getByRole('radio').last().boundingBox();
- expect(rowBox.x + rowBox.width - (lastBox.x + lastBox.width)).toBeLessThan(40);
+ expect(lastBox.x + lastBox.width).toBeLessThanOrEqual(rowBox.x + rowBox.width);
await expect(picker.getByRole('radio', { checked: true })).toHaveAccessibleName('Wind grey');
await expect(picker.locator('.nm')).toHaveCount(0); // cards only, no names (owner decision)
// Each card is drawn in its own theme: the mini windows differ.
diff --git a/ui/tests/responsive.spec.js b/ui/tests/responsive.spec.js
new file mode 100644
index 00000000..a04a67c4
--- /dev/null
+++ b/ui/tests/responsive.spec.js
@@ -0,0 +1,106 @@
+// Responsive layout: no horizontal overflow of the page and no control sticking out of its card at
+// window sizes from a narrow strip to a very wide monitor, on every page, search and onboarding.
+import { test, expect } from '@playwright/test';
+
+const SIZES = [[420, 700], [560, 800], [820, 560], [900, 360], [1000, 1400], [1280, 800], [2560, 1440]];
+
+const boot = (extra = {}) => async (page) => {
+ await page.addInitScript((extra) => {
+ const listeners = new Set();
+ const live = { maxLevel: '12', zoomInVk: '33', zoomInButton: '2', zoomOutVk: '114', cursorLockVk: '113',
+ model: 'hybrid', uiTheme: 'dark', showAdvanced: '1', ...extra };
+ window.chrome = { webview: {
+ addEventListener: (_e, fn) => listeners.add(fn),
+ postMessage: (msg) => {
+ if (msg.type === 'getConfig') listeners.forEach((fn) => fn({ data: { type: 'config', values: { ...live }, saved: { ...live },
+ profiles: { names: ['Default', 'Gaming'], active: 'Default' } } }));
+ },
+ } };
+ }, extra);
+};
+
+// Returns problems: page-level horizontal overflow, and any control or text wider than its card.
+const problems = (page) => page.evaluate(() => {
+ const out = [];
+ const de = document.documentElement;
+ if (de.scrollWidth > de.clientWidth) out.push('document scrolls horizontally');
+ const main = document.querySelector('main.main');
+ if (main && main.scrollWidth > main.clientWidth) out.push('main scrolls horizontally');
+ for (const card of document.querySelectorAll('.card')) {
+ const cr = card.getBoundingClientRect();
+ for (const el of card.querySelectorAll('.row .meta, .row .ctl, .row .ctl *')) {
+ const r = el.getBoundingClientRect();
+ if (r.width === 0 || r.height === 0) continue;
+ if (r.right > cr.right + 1 || r.left < cr.left - 1) out.push((el.className || el.tagName) + ' leaves its card');
+ }
+ }
+ return out;
+});
+
+for (const [w, h] of SIZES) {
+ test(`no overflow at ${w}x${h}`, async ({ page }) => {
+ await boot()(page);
+ await page.setViewportSize({ width: w, height: h });
+ await page.goto('/');
+ await expect(page.locator('h1')).toHaveText('Hotkeys');
+ for (const g of ['hotkeys', 'zoom', 'view', 'prefs', 'tray', 'about']) {
+ await page.locator('.side .it[data-g="' + g + '"]').click();
+ await page.waitForTimeout(150);
+ expect(await problems(page), g).toEqual([]);
+ }
+ await page.locator('.side .search input').fill('zoom');
+ await page.waitForTimeout(150);
+ expect(await problems(page), 'search').toEqual([]);
+ });
+}
+
+test('wide window caps the content width', async ({ page }) => {
+ await boot()(page);
+ await page.setViewportSize({ width: 2560, height: 1440 });
+ await page.goto('/');
+ await page.locator('.side .it[data-g="zoom"]').click();
+ const w = await page.locator('.card').first().evaluate((el) => el.getBoundingClientRect().width);
+ expect(w).toBeLessThanOrEqual(960);
+});
+
+test('narrow window moves the sidebar above the page', async ({ page }) => {
+ await boot()(page);
+ await page.setViewportSize({ width: 420, height: 700 });
+ await page.goto('/');
+ const side = await page.locator('.side').evaluate((el) => el.getBoundingClientRect().width);
+ expect(side).toBeGreaterThan(380);
+});
+
+for (const [w, h] of [[420, 700], [900, 360]]) {
+ test(`onboarding fits ${w}x${h}`, async ({ page }) => {
+ await boot({ uiTheme: 'auto' })(page);
+ await page.setViewportSize({ width: w, height: h });
+ await page.goto('/?mode=onboard');
+ await expect(page.getByRole('button', { name: 'Get started' })).toBeVisible();
+ for (const name of ['Get started', 'Next']) {
+ await page.getByRole('button', { name }).click();
+ await page.waitForTimeout(300);
+ const fits = await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth);
+ expect(fits).toBe(true);
+ }
+ });
+}
+
+for (const [w, h] of [[820, 560], [900, 600]]) {
+ test(`subtexts stay on one line at ${w}x${h}`, async ({ page }) => {
+ await boot()(page);
+ await page.setViewportSize({ width: w, height: h });
+ await page.goto('/');
+ for (const g of ['hotkeys', 'zoom', 'view', 'prefs', 'tray', 'about']) {
+ await page.locator('.side .it[data-g="' + g + '"]').click();
+ await page.waitForTimeout(150);
+ const wrapped = await page.evaluate(() => [...document.querySelectorAll('.desc, .bdesc, .thint')].filter((el) => {
+ const r = el.getBoundingClientRect();
+ if (r.width === 0 || r.height === 0) return false;
+ const lh = parseFloat(getComputedStyle(el).lineHeight) || parseFloat(getComputedStyle(el).fontSize) * 1.3;
+ return r.height > lh * 1.5;
+ }).map((el) => el.textContent));
+ expect(wrapped, g).toEqual([]);
+ }
+ });
+}
diff --git a/ui/tests/schema.spec.js b/ui/tests/schema.spec.js
index a23d195d..9b275e05 100644
--- a/ui/tests/schema.spec.js
+++ b/ui/tests/schema.spec.js
@@ -10,7 +10,7 @@ const OLD_KEYS = [
'__zoomIn', '__zoomOut', '__zoomWheel', '__pan', // was four rows (Pan left/right/up/down); their ini keys live in OLD_BIND_KEYS
'noSwallowApps', 'maxLevel', 'zoomInSpeed', 'zoomOutSpeed', 'panSpeed', 'smoothZoomAccel',
'smoothZoomRamp', 'zoomEaseOutMs', 'txSamplingMode', '__hideCursor', '__cursorLock', 'lockApps',
- 'cursorSensitivity', 'panGlideMaxPx', 'panGlideMs', 'trackCaret', 'trackFocus', 'trackAlign', 'mouseAlign',
+ 'cursorSensitivity', 'panGlideMaxPx', 'trackCaret', 'trackFocus', 'trackAlign', 'mouseAlign',
'mouseMarginPct', 'colorWarmPct', 'colorDimPct', 'model', 'engineGame', 'engineAcrylic',
'engineDesktop', 'engineOther', 'renderExclude', 'diagnostics', '__about',
];
@@ -57,7 +57,7 @@ test('a captioned section has two or more rows, and the advanced rows are the on
if (base.length) expect(base.length, g.id + ' / ' + c.caption + ' (advanced off)').toBeGreaterThanOrEqual(2);
}
expect(allRows.filter((r) => r.adv).map((r) => r.key).sort()).toEqual(
- ['engineAcrylic', 'engineDesktop', 'engineGame', 'engineOther', 'lockApps', 'model', 'noSwallowApps', 'panGlideMs', 'smoothZoomAccel', 'smoothZoomRamp'].sort());
+ ['engineAcrylic', 'engineDesktop', 'engineGame', 'engineOther', 'lockApps', 'model', 'noSwallowApps', 'smoothZoomAccel', 'smoothZoomRamp'].sort());
// Troubleshooting is never advanced, and the extra keys carry their switch key.
for (const k of ['diagnostics', '__diagnostics', '__openIni']) expect(allRows.find((r) => r.key === k).adv).toBeUndefined();
expect(Object.fromEntries(allRows.filter((r) => r.onKey).map((r) => [r.key, r.onKey]))).toEqual(
diff --git a/ui/tests/settings.spec.js b/ui/tests/settings.spec.js
index 8ea146be..969afdc2 100644
--- a/ui/tests/settings.spec.js
+++ b/ui/tests/settings.spec.js
@@ -83,14 +83,14 @@ test('pages show their sections; a section with nothing to show is dropped', asy
test('copy: the row names and descriptions follow the table, and "Never use Render for" is gone', async ({ page }) => {
await page.goto('/');
await expect(page.locator('[data-key="__zoomIn"] .label')).toHaveText('Zoom in');
- await expect(page.locator('[data-key="__zoomIn"] .desc')).toHaveText('Hold or scroll to magnify the view.');
+ await expect(page.locator('[data-key="__zoomIn"] .desc')).toHaveText('Hold a key or scroll to zoom in');
await expect(page.locator('[data-key="__hideCursor"] .label')).toHaveText('Hide pointer');
- await expect(page.locator('[data-key="__pan"] .desc')).toHaveText('Hold the modifiers and press an arrow key to move the view.');
+ await expect(page.locator('[data-key="__pan"] .desc')).toHaveText('Hold modifier keys and an arrow to pan');
await advanced(page, true);
await go(page, 'zoom');
await expect(page.locator('main .cap')).toHaveText(['Level and speed', 'Easing', 'Engine']);
await expect(page.locator('[data-key="smoothZoomAccel"] .label')).toHaveText('Soft start');
- await expect(page.locator('[data-key="engineGame"] .desc')).toHaveText('Full-screen and borderless games.');
+ await expect(page.locator('[data-key="engineGame"] .desc')).toHaveText('Full screen and borderless games');
await expect(page.getByText('Never use Render for')).toHaveCount(0);
await expect(key(page, 'renderExclude')).toHaveCount(0);
});
diff --git a/ui/tests/tray.spec.js b/ui/tests/tray.spec.js
index a679f026..48acd640 100644
--- a/ui/tests/tray.spec.js
+++ b/ui/tests/tray.spec.js
@@ -81,7 +81,7 @@ test('tab: below the sidebar divider, banner, performance card and both lists',
await expect(page.locator('.side nav:not(.bottom) .it[data-g="tray"]')).toHaveCount(0);
await page.locator('.side .it[data-g="tray"]').click();
await expect(page.locator('h1')).toHaveText('Tray menu');
- await expect(page.locator('.bdesc')).toHaveText('Choose what the tray menu shows, and in what order.');
+ await expect(page.locator('.bdesc')).toHaveText('What the tray menu shows and in what order');
await expect(page.getByRole('switch', { name: 'Performance in the tray' })).toBeChecked(); // on by default (#329)
await expect(page.locator('#cap-sliders')).toHaveText('Sliders');
await expect(page.locator('#cap-toggles')).toHaveText('Toggles');
@@ -129,11 +129,11 @@ test('check and uncheck write the enabled list and the full order', async ({ pag
await expect(page.locator('[data-cnt="toggles"]')).toHaveText('3 on');
});
-test('slider cap: at four the unchecked boxes are disabled with a note, unchecking frees them', async ({ page }) => {
+test('slider cap: at four the unchecked boxes are disabled and the count says full', async ({ page }) => {
await open(page);
await chk(page, 'sliders', 'Max zoom').click();
await chk(page, 'sliders', 'Arrow key speed').click();
- await expect(page.locator('[data-cnt="sliders"]')).toHaveText('4 of 4 · Uncheck one to add another');
+ await expect(page.locator('[data-cnt="sliders"]')).toHaveText('4 of 4 full');
await expect(chk(page, 'sliders', 'Zoom-in speed')).toBeDisabled();
await expect(chk(page, 'sliders', 'Release glide')).toBeDisabled();
await expect(chk(page, 'sliders', 'Warmth')).toBeEnabled();