diff --git a/docs/architecture/05-transform-engine.md b/docs/architecture/05-transform-engine.md index d4fdbb7..9904d26 100644 --- a/docs/architecture/05-transform-engine.md +++ b/docs/architecture/05-transform-engine.md @@ -262,9 +262,14 @@ DWM magnifies with nearest neighbour unless something calls `quickZoomDefault`): above it the jumps reach 15-25 px and the pointer runs off the screen. Report: `Documents\Claude\research\wind\2026-10-10-high-zoom-shake.md`. -- No release glide with the high resolution cursor (#427): the ladder could only cut a glide short - or let its slow tail cross rounding steps (which shook the image and showed it doubled, closed - #426), so `zoomEaseOutMs` is not applied while `txSamplingMode=1`; the zoom stops on release. +- Release glide at both sampling modes (#439, measured 2026-10-10). The high resolution cursor + had its glide turned off (#427) because the slow tail shook and showed the image doubled; the + doubling was the ladder stepping back below a request it had overtaken (#429, `LadderDir`). The + glide is back and stops just before its slow tail (`EaseOutShouldStop`, on the level on screen): + 240-260 ms at zoomInSpeed 2.7 / 115 ms, no backward step, pointer steady. The full tail runs + 480-550 ms but lands in 0.5-0.9 % ladder hops, which the field read as shake; snapping it to + the exact glide level (`stepRel` capped at the glide's own motion) eases perfectly but shook + 11-13 px. Both rejected. - Nearest while the level moves and smooth at rest was tried: steady, but the switch from pixel to smooth is plainly visible, so it was rejected (2026-10-07). The older "swap shifted the image 1-2 px" verdict predates the working setter and is void. diff --git a/src/main.cpp b/src/main.cpp index 8a4c88e..90a0067 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1295,10 +1295,12 @@ static void RunTick(TickState& t) { // root-caused elsewhere - issue #148 - so the user's configured speed applies everywhere.) t.zoom.setProfile(t.cfg.zoomInSpeed, t.cfg.zoomOutSpeed, t.cfg.smoothZoom != 0, t.cfg.smoothZoomAccel, t.cfg.smoothZoomRamp); - // No release glide with the high resolution cursor (#427): its ladder can only cut a glide short - // (EaseOutShouldStop) or let the slow tail cross DWM's rounding steps, which shook the image and - // showed it doubled (field 2026-10-10, closed #426). The zoom stops on release instead. - t.zoom.setEaseOut(t.cfg.txSamplingMode == 1 ? 0.0 : t.cfg.zoomEaseOutMs / 1000.0); + // The release glide runs at both sampling modes (#439). With the high resolution cursor it stops + // just before its slow tail (EaseOutShouldStop below): the full tail lands in ladder hops, which + // the field reads as shake (2026-10-10). Turned off in #427 because the tail also showed the image + // doubled; that was the ladder stepping back below a request it had overtaken (#429, LadderDir). + // Measured with the cut (speed 2.7, 115 ms): 240-260 ms of glide, no backward step, pointer steady. + t.zoom.setEaseOut(t.cfg.zoomEaseOutMs / 1000.0); // Quick-zoom trigger. Modifier mode (quickZoomHotkeyMode==0): hold the configured modifier // (Ctrl/Alt/Shift; "None" = off) and tap a zoom key. While the modifier is held it toggles quick // zoom (below) instead of hold-zooming, so suppress the hold-zoom direction (the toggle snaps the diff --git a/src/version.h b/src/version.h index 1ba9f14..b5bf7e5 100644 --- a/src/version.h +++ b/src/version.h @@ -3,8 +3,8 @@ #pragma once #define WIND_VER_MAJOR 0 -#define WIND_VER_MINOR 27 -#define WIND_VER_PATCH 1 +#define WIND_VER_MINOR 28 +#define WIND_VER_PATCH 0 // String form for logs/snapshot/UI. Keep in sync with the numeric parts above. -#define WIND_VERSION_STR "0.27.1" +#define WIND_VERSION_STR "0.28.0" diff --git a/ui/src/settings-schema.js b/ui/src/settings-schema.js index 77bb14a..3481d54 100644 --- a/ui/src/settings-schema.js +++ b/ui/src/settings-schema.js @@ -66,8 +66,7 @@ export const groups = [ { key: 'maxLevel', type: 'slider', label: 'Max zoom', keywords: ['maximum', 'limit', 'highest', 'magnification', 'level', 'cap', 'zoom level', 'factor', 'times', 'upper'], desc: 'The highest zoom level', min: 2, max: 30, step: 1, def: 12.0, unit: 'times' }, { key: 'zoomInSpeed', type: 'slider', label: 'Zoom-in speed', keywords: ['faster', 'slower', 'rate', 'magnify speed', 'zoom rate', 'acceleration', 'velocity', 'how fast'], desc: 'How fast the view zooms in', min: 0.25, max: 4, step: 0.05, def: 1.0, unit: 'times' }, { key: 'zoomOutSpeed', type: 'slider', label: 'Zoom-out speed', keywords: ['faster', 'slower', 'rate', 'zoom rate', 'velocity', 'how fast'], desc: 'How fast the view zooms out', min: 0.25, max: 4, step: 0.05, def: 1.0, unit: 'times' }, - { key: 'zoomEaseOutMs', type: 'slider', label: 'Release glide', keywords: ['inertia', 'coast', 'momentum', 'ease out', 'easing', 'smooth stop', 'slow down', 'deceleration', 'stop', 'let go', 'release', 'delay', 'milliseconds'], desc: 'How long the zoom coasts after you let go', min: 0, max: 300, step: 5, def: 45, unit: 'ms', - offIf: { key: 'txSamplingMode', eq: '1' } }, + { key: 'zoomEaseOutMs', type: 'slider', label: 'Release glide', keywords: ['inertia', 'coast', 'momentum', 'ease out', 'easing', 'smooth stop', 'slow down', 'deceleration', 'stop', 'let go', 'release', 'delay', 'milliseconds'], desc: 'How long the zoom coasts after you let go', min: 0, max: 300, step: 5, def: 45, unit: 'ms' }, ] }, { caption: 'Easing', rows: [ // Smooth zoom is always on (core default 1); its shape sliders are advanced. diff --git a/ui/tests/settings.spec.js b/ui/tests/settings.spec.js index 969afdc..a2dac0a 100644 --- a/ui/tests/settings.spec.js +++ b/ui/tests/settings.spec.js @@ -185,17 +185,14 @@ test('View: the edge margin shows only when the pointer is kept within the edges await expect(key(page, 'mouseMarginPct')).toBeVisible(); }); -test('High resolution cursor is tagged Experimental and turns the release glide off (#427)', async ({ page }) => { +test('High resolution cursor is tagged Experimental and keeps the release glide (#439)', async ({ page }) => { await page.goto('/'); await go(page, 'view'); await expect(key(page, 'txSamplingMode').locator('.tag')).toHaveText('Experimental'); - await go(page, 'zoom'); - await expect(key(page, 'zoomEaseOutMs')).not.toHaveClass(/disabled/); - await go(page, 'view'); await key(page, 'txSamplingMode').getByRole('switch').check({ force: true }); await go(page, 'zoom'); - await expect(key(page, 'zoomEaseOutMs')).toHaveClass(/disabled/); - await expect(key(page, 'zoomEaseOutMs').locator('input[type=range]')).toBeDisabled(); + await expect(key(page, 'zoomEaseOutMs')).not.toHaveClass(/disabled/); + await expect(key(page, 'zoomEaseOutMs').locator('input[type=range]')).toBeEnabled(); }); test('Typing and focus: caret on, focus off by default; a toggle writes at once', async ({ page }) => {