diff --git a/apps/readest-app/src/__tests__/app/reader/hooks/useAutoPageTurn.test.ts b/apps/readest-app/src/__tests__/app/reader/hooks/useAutoPageTurn.test.ts index d6835c47..e1e48f90 100644 --- a/apps/readest-app/src/__tests__/app/reader/hooks/useAutoPageTurn.test.ts +++ b/apps/readest-app/src/__tests__/app/reader/hooks/useAutoPageTurn.test.ts @@ -2,8 +2,8 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { cleanup, renderHook } from '@testing-library/react'; const DWELL_MS = 500; -// The mocked reading frame is 1000x1000; with the 0.15 quarter-ellipse corner, -// (920,920) sits in the bottom-right and (80,80) in the top-left. +// The mocked reading frame is 1000x1000; the corner zone is capped at 50px, so +// (970,970) sits in the bottom-right and (30,30) in the top-left. const VW = 1000; const h = vi.hoisted(() => ({ @@ -58,7 +58,7 @@ afterEach(() => { describe('useAutoPageTurn corner-dwell page turn (decoupled from DOM selection)', () => { test('turns to the next page after a point dwells in the bottom-right corner', async () => { const { result } = setup(); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); @@ -67,7 +67,7 @@ describe('useAutoPageTurn corner-dwell page turn (decoupled from DOM selection)' test('turns to the previous page when a point dwells in the top-left corner', async () => { const { result } = setup(); - result.current.noteAutoTurnPoint({ x: 80, y: 80 }); + result.current.noteAutoTurnPoint({ x: 30, y: 30 }); await advance(); expect(h.view.prev).toHaveBeenCalledTimes(1); @@ -85,7 +85,7 @@ describe('useAutoPageTurn corner-dwell page turn (decoupled from DOM selection)' test('does not turn until the dwell has elapsed', async () => { const { result } = setup(); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); await vi.advanceTimersByTimeAsync(DWELL_MS - 100); expect(h.view.next).not.toHaveBeenCalled(); @@ -93,7 +93,7 @@ describe('useAutoPageTurn corner-dwell page turn (decoupled from DOM selection)' test('cancels the turn if the point leaves the corner before the dwell', async () => { const { result } = setup(); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); result.current.noteAutoTurnPoint({ x: 500, y: 500 }); await advance(); @@ -102,7 +102,7 @@ describe('useAutoPageTurn corner-dwell page turn (decoupled from DOM selection)' test('cancel() drops a pending turn', async () => { const { result } = setup(); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); result.current.cancel(); await advance(); @@ -111,21 +111,21 @@ describe('useAutoPageTurn corner-dwell page turn (decoupled from DOM selection)' test('turns one page per engagement and does not repeat while held', async () => { const { result } = setup(); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); }); test('re-arms after the point leaves the corner and returns', async () => { const { result } = setup(); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); await advance(); result.current.noteAutoTurnPoint({ x: 500, y: 500 }); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); await advance(); expect(h.view.next).toHaveBeenCalledTimes(2); @@ -133,7 +133,7 @@ describe('useAutoPageTurn corner-dwell page turn (decoupled from DOM selection)' test('null disengages the corner', async () => { const { result } = setup(); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); result.current.noteAutoTurnPoint(null); await advance(); @@ -146,7 +146,7 @@ describe('useAutoPageTurn corner-dwell page turn (decoupled from DOM selection)' await advance(); expect(h.view.next).not.toHaveBeenCalled(); - result.current.noteAutoTurnPoint({ x: 860, y: 860 }); + result.current.noteAutoTurnPoint({ x: 870, y: 870 }); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); }); @@ -156,14 +156,14 @@ describe('useAutoPageTurn corner-dwell page turn (decoupled from DOM selection)' const cb = vi.fn(); const unsub = result.current.onAfterTurn(cb); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); await advance(); expect(cb).toHaveBeenCalledTimes(1); expect(cb).toHaveBeenCalledWith('br'); unsub(); result.current.noteAutoTurnPoint({ x: 500, y: 500 }); - result.current.noteAutoTurnPoint({ x: 920, y: 920 }); + result.current.noteAutoTurnPoint({ x: 970, y: 970 }); await advance(); expect(cb).toHaveBeenCalledTimes(1); }); @@ -187,11 +187,22 @@ describe('useAutoPageTurn corner-dwell page turn (decoupled from DOM selection)' test('cornerAtPoint maps a window point to its corner', () => { const { result } = setup(); - expect(result.current.cornerAtPoint({ x: 920, y: 920 })).toBe('br'); - expect(result.current.cornerAtPoint({ x: 80, y: 80 })).toBe('tl'); + expect(result.current.cornerAtPoint({ x: 970, y: 970 })).toBe('br'); + expect(result.current.cornerAtPoint({ x: 30, y: 30 })).toBe('tl'); expect(result.current.cornerAtPoint({ x: 500, y: 500 })).toBe(null); expect(result.current.cornerAtPoint(null)).toBe(null); }); + + test('caps the corner zone to AUTO_TURN_CORNER_MAX_PX on a wide reading area', async () => { + // On the 1000px frame the 0.15 fraction would reach 150px from the corner, + // pulling (900,900) into the bottom-right zone; the 50px cap keeps it out so + // selections that merely end near the page edge on wide screens don't turn. + const { result } = setup(); + expect(result.current.cornerAtPoint({ x: 900, y: 900 })).toBe(null); + result.current.noteAutoTurnPoint({ x: 900, y: 900 }); + await advance(); + expect(h.view.next).not.toHaveBeenCalled(); + }); }); const fullArea = { left: 0, top: 0, right: VW, bottom: VW, width: VW, height: VW } as DOMRect; diff --git a/apps/readest-app/src/__tests__/app/reader/hooks/useTextSelector-autoTurn.test.ts b/apps/readest-app/src/__tests__/app/reader/hooks/useTextSelector-autoTurn.test.ts index 031327de..be34c1a9 100644 --- a/apps/readest-app/src/__tests__/app/reader/hooks/useTextSelector-autoTurn.test.ts +++ b/apps/readest-app/src/__tests__/app/reader/hooks/useTextSelector-autoTurn.test.ts @@ -2,8 +2,8 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { cleanup, renderHook } from '@testing-library/react'; const DWELL_MS = 500; -// The mocked reading frame is 1000x1000; with the 0.15 quarter-ellipse corner, -// (920,920) sits in the bottom-right and (80,80) in the top-left. +// The mocked reading frame is 1000x1000; the corner zone is capped at 50px, so +// (970,970) sits in the bottom-right and (30,30) in the top-left. const VW = 1000; const h = vi.hoisted(() => ({ @@ -148,7 +148,7 @@ afterEach(() => { describe('useTextSelector auto page-turn on corner dwell (#1354)', () => { test('turns to the next page when a signal dwells in the bottom-right corner', async () => { const { result } = setup(); - pointerMove(result, 920, 920); + pointerMove(result, 970, 970); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); @@ -157,7 +157,7 @@ describe('useTextSelector auto page-turn on corner dwell (#1354)', () => { test('turns to the previous page (not goLeft) in the top-left corner', async () => { const { result } = setup(); - pointerMove(result, 80, 80); + pointerMove(result, 30, 30); await advance(); expect(h.view.prev).toHaveBeenCalledTimes(1); @@ -176,7 +176,7 @@ describe('useTextSelector auto page-turn on corner dwell (#1354)', () => { test('does not turn until the dwell has elapsed', async () => { const { result } = setup(); - pointerMove(result, 920, 920); + pointerMove(result, 970, 970); await vi.advanceTimersByTimeAsync(DWELL_MS - 100); expect(h.view.next).not.toHaveBeenCalled(); @@ -184,7 +184,7 @@ describe('useTextSelector auto page-turn on corner dwell (#1354)', () => { test('cancels the turn if the pointer leaves the corner before the dwell', async () => { const { result } = setup(); - pointerMove(result, 920, 920); // arms + pointerMove(result, 970, 970); // arms pointerMove(result, 500, 500); // leaves the corner -> disengage await advance(); @@ -193,24 +193,24 @@ describe('useTextSelector auto page-turn on corner dwell (#1354)', () => { test('turns one page per engagement and does not repeat while held', async () => { const { result } = setup(); - pointerMove(result, 920, 920); + pointerMove(result, 970, 970); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); // Still held in the same corner -> no further turns. - pointerMove(result, 920, 920); + pointerMove(result, 970, 970); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); }); test('re-arms after the pointer leaves the corner and returns', async () => { const { result } = setup(); - pointerMove(result, 920, 920); + pointerMove(result, 970, 970); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); pointerMove(result, 500, 500); // leave - pointerMove(result, 920, 920); // return + pointerMove(result, 970, 970); // return await advance(); expect(h.view.next).toHaveBeenCalledTimes(2); }); @@ -226,7 +226,7 @@ describe('useTextSelector auto page-turn on corner dwell (#1354)', () => { test('does not auto-turn in scrolled mode', async () => { h.viewSettings = { scrolled: true }; const { result } = setup(); - pointerMove(result, 920, 920); + pointerMove(result, 970, 970); await advance(); expect(h.view.next).not.toHaveBeenCalled(); @@ -242,31 +242,31 @@ describe('useTextSelector auto page-turn on corner dwell (#1354)', () => { test('the selection caret is also an engagement signal', async () => { const { result } = setup(); - caretMove(result, 920, 920); + caretMove(result, 970, 970); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); }); test('measures corners against the content-inset reading area', async () => { - // A 100px inset shrinks the frame to [100,900]: the old outer corner (960,960) - // now falls outside the area, while (860,860) is inside the inset corner. + // A 100px inset shrinks the frame to [100,900]: the outer corner (960,960) + // now falls outside the area, while (870,870) is inside the inset corner. const { result } = setup({ top: 100, right: 100, bottom: 100, left: 100 }); pointerMove(result, 960, 960); await advance(); expect(h.view.next).not.toHaveBeenCalled(); - pointerMove(result, 860, 860); + pointerMove(result, 870, 870); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); }); test('maps the pointer through the iframe offset (multi-column page)', async () => { - // The iframe is scrolled into a later column: a pointer at clientX=1620 maps - // to window x=920 (1620-700), landing in the bottom-right corner. + // The iframe is scrolled into a later column: a pointer at clientX=1670 maps + // to window x=970 (1670-700), landing in the bottom-right corner. frameOffset = { left: -700, top: 0 }; const { result } = setup(); - pointerMove(result, 1620, 920); + pointerMove(result, 1670, 970); await advance(); expect(h.view.next).toHaveBeenCalledTimes(1); diff --git a/apps/readest-app/src/__tests__/app/reader/hooks/useTextSelector-instantTurn.test.ts b/apps/readest-app/src/__tests__/app/reader/hooks/useTextSelector-instantTurn.test.ts index f5494d38..fcab6252 100644 --- a/apps/readest-app/src/__tests__/app/reader/hooks/useTextSelector-instantTurn.test.ts +++ b/apps/readest-app/src/__tests__/app/reader/hooks/useTextSelector-instantTurn.test.ts @@ -141,7 +141,7 @@ describe('useTextSelector cross-page instant highlight (corner auto-turn)', () = test('turns to the next page when the instant-highlight drag dwells in the bottom-right corner', async () => { const { result } = setup(); - engageAndMoveTo(result, 920, 920); + engageAndMoveTo(result, 970, 970); expect(result.current.isInstantAnnotating.current).toBe(true); await advance(); @@ -151,7 +151,7 @@ describe('useTextSelector cross-page instant highlight (corner auto-turn)', () = test('turns to the previous page when the drag dwells in the top-left corner', async () => { const { result } = setup(); - engageAndMoveTo(result, 80, 80); + engageAndMoveTo(result, 30, 30); await advance(); expect(h.view.prev).toHaveBeenCalledTimes(1); @@ -170,7 +170,7 @@ describe('useTextSelector cross-page instant highlight (corner auto-turn)', () = test('does not auto-turn in scrolled mode', async () => { h.viewSettings = { scrolled: true }; const { result } = setup(); - engageAndMoveTo(result, 920, 920); + engageAndMoveTo(result, 970, 970); await advance(); expect(h.view.next).not.toHaveBeenCalled(); diff --git a/apps/readest-app/src/app/reader/hooks/useAutoPageTurn.ts b/apps/readest-app/src/app/reader/hooks/useAutoPageTurn.ts index 4008514b..a6e39ea3 100644 --- a/apps/readest-app/src/app/reader/hooks/useAutoPageTurn.ts +++ b/apps/readest-app/src/app/reader/hooks/useAutoPageTurn.ts @@ -13,6 +13,11 @@ export const AUTO_TURN_DWELL_MS = 500; // larger/rectangular zone catches normal selections that end in the lower-right // of the page and turns the page unexpectedly. export const AUTO_TURN_CORNER_FRACTION = 0.15; +// Hard ceiling on the corner radius in pixels. The fraction alone grows the zone +// with the reading area, so on a wide screen (desktop, multi-column pages) it +// reaches deep into the text and turns the page when a selection merely ends near +// the edge. Cap each axis so the zone stays a corner regardless of page size. +export const AUTO_TURN_CORNER_MAX_PX = 50; export type Corner = 'br' | 'tl'; export type Point = { x: number; y: number }; @@ -30,8 +35,8 @@ export interface AutoTurnControls { // corner. Returns null when the point is in neither. const cornerOf = (x: number, y: number, w: number, h: number): Corner | null => { if (w <= 0 || h <= 0) return null; - const rx = w * AUTO_TURN_CORNER_FRACTION; - const ry = h * AUTO_TURN_CORNER_FRACTION; + const rx = Math.min(w * AUTO_TURN_CORNER_FRACTION, AUTO_TURN_CORNER_MAX_PX); + const ry = Math.min(h * AUTO_TURN_CORNER_FRACTION, AUTO_TURN_CORNER_MAX_PX); const inEllipse = (dx: number, dy: number) => (dx / rx) ** 2 + (dy / ry) ** 2 <= 1; if (inEllipse(w - x, h - y)) return 'br'; if (inEllipse(x, y)) return 'tl';