Skip to content

fix(editor): keyboard seeks scroll the view on a HiDPI display - #320

Merged
ChrisBeWithYou merged 1 commit into
mainfrom
fix/editor-seek-viewport-dpr
Jul 19, 2026
Merged

fix(editor): keyboard seeks scroll the view on a HiDPI display#320
ChrisBeWithYou merged 1 commit into
mainfrom
fix/editor-seek-viewport-dpr

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

canvas.width is device pixels — main.js:1407 sets it to cssWidth * DPR — but _editorSeekToTime mixed it straight into LABEL_W and S.zoom, which are both CSS-pixel units.

On a 2× display that put the computed right edge a full viewport past the real one: an 800px-CSS canvas measured 12.9s of timeline instead of 6.2s. So seeking to a cursor that had visibly run off the right never fired the scroll branch, and the left-hand branch used a margin twice as wide as intended.

The follow path in audio.js:936 has always divided by DPR. This makes the seek path use the same viewport.

Repro (on main, at DPR 2)

DPR = 2 | real right edge = 6.23 s
seek to 8s -> scrollX = 0.000     <-- view never followed

Tests

tests/seek_viewport_dpr.test.mjs installs a DPR-2 window before the module graph loads, then drives the real _editorSeekToTime. On main:

  • seeking past the right edge scrolls the view on a 2x display
  • seeking left of the view scrolls back by a CSS-pixel margin

The inside-the-view and cursor-clamp cases pass on main and pin what must not move.

Notes

At DPR 1 with a canvas present the arithmetic is unchanged, so standard-density displays are byte-identical. The no-canvas fallback (tests only) is unified — the old code fell back to 10 for the margin and 800 for the edge, which disagreed with each other.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved keyboard seeking and timeline scrolling on HiDPI displays.
    • Corrected right-edge detection and left-margin behavior when device pixel ratios are higher than 1.
    • Ensured cursor positioning remains within the available timeline range.
  • Tests

    • Added coverage for high-density display seeking, scrolling boundaries, and cursor clamping.
  • Documentation

    • Added a changelog entry describing the HiDPI seeking fix.

canvas.width is DEVICE pixels — main.js sets it to cssWidth * DPR — but
_editorSeekToTime mixed it straight into LABEL_W and S.zoom, which are both
CSS-pixel units. On a 2x display that put the computed right edge a full
viewport past the real one (an 800px-CSS canvas measured 12.9s of timeline
instead of 6.2s), so seeking to a cursor that had visibly run off the right
never fired the scroll branch, and the left-hand branch used a margin twice
as wide as intended.

The viewport is now read in CSS pixels once (canvas.width / DPR) and used for
both the margin and the right edge — the same units the follow path in
audio.js has always used. At DPR 1 the arithmetic is unchanged, so standard
-density displays are byte-identical.

Tests: tests/seek_viewport_dpr.test.mjs — installs a DPR-2 window before the
module graph loads, then drives the real _editorSeekToTime. The past-the-right
-edge and left-margin cases both fail on main; the inside-the-view and cursor
-clamp cases pin the behaviour that must not move.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 214831e4-16df-403a-91b8-56ebebb5c36b

📥 Commits

Reviewing files that changed from the base of the PR and between 3b1be44 and 9bfac13.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/input.js
  • tests/seek_viewport_dpr.test.mjs

📝 Walkthrough

Walkthrough

Keyboard seek viewport boundaries now use CSS pixels derived from the canvas width and device pixel ratio. New HiDPI tests cover viewport duration, scrolling in both directions, unchanged in-view positions, and cursor clamping.

Changes

HiDPI keyboard seeking

Layer / File(s) Summary
CSS-pixel viewport calculation
src/input.js, CHANGELOG.md
_editorSeekToTime() uses canvas.width / DPR for viewport and scroll-boundary calculations, with the fix recorded in the changelog.
HiDPI seek regression coverage
tests/seek_viewport_dpr.test.mjs
Tests use a 2x device-pixel fixture to validate viewport duration, directional scrolling, unchanged in-view positions, and cursor-time clamping.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: byrongamatos

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: keyboard seek scrolling on HiDPI displays.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/editor-seek-viewport-dpr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ChrisBeWithYou
ChrisBeWithYou merged commit a368ea3 into main Jul 19, 2026
4 checks passed
@ChrisBeWithYou
ChrisBeWithYou deleted the fix/editor-seek-viewport-dpr branch July 19, 2026 08:13
ChrisBeWithYou pushed a commit that referenced this pull request Jul 19, 2026
…ll in Play

Two things, from a look at how Logic/Ableton/Pro Tools handle the playhead
running off-screen during playback.

THE BUG (Christian: "doesn't respect the edge at different resolutions").
_followScrollTargetPure measured its 80% trigger and 30% landing against the
FULL canvas width, but read the cursor x from timeToX — which adds the 52px
LABEL_W gutter. So the usable timeline (viewW - 52) was never what it computed
against, and the effective landing fraction drifted with canvas width: 52px is
a bigger slice of an 800px canvas than a 1920px one. Real, but modest — a
constant ~16px, ~1-2% across resolutions, not a gross failure. Both the forward
follow and the loop-wrap recentre now measure against the usable width, so the
playhead lands at the same on-screen spot at every resolution.

Two things the DAW survey settled, worth recording:
  - The DPR half of the hypothesis was WRONG for this path — both cursorX and
    viewW are already CSS px (main.js scales the context by DPR). Adding a DPR
    term here would REINTRODUCE a bug (that was #320's defect, in input.js).
    Deliberately no DPR term.
  - The dramatic "ignores the edge at some window sizes" symptom is most likely
    a STALE canvas width, which #328 (the wrap ResizeObserver) fixes — not this
    formula. This PR is the gutter-math polish; #328 is the other half.

THE FEATURE. Follow now has two manners, matching Logic: page-jump (default,
unchanged) and continuous "Scroll in Play" — the playhead pins at centre and
the timeline glides under it. All three DAWs converge on exactly this page-vs-
continuous choice and offer nothing more, so it's the whole customization worth
having; Logic's full Catch matrix would be over-engineering for one window.

Surfaced as a View-menu checkbox (not a second button, not a 3-state cycle —
the Follow button's aria-pressed has no honest third value), dimmed while
Follow is off, honouring prefers-reduced-motion by falling back to the page
jump. The Follow button's tooltip and the status line name the live manner.

The label-gutter edge is now a faint divider in the chart, so the usable
timeline reads as bounded and a future gutter slip is visible on sight. The
continuous pin is drawn from _followPinXPure with the SAME fraction the scroll
target uses, so the pin can never point where the scroll doesn't hold.

Follow is global (moves S.scrollX), so one fix covers every time-axis view;
Tab view (alphaTab owns its own cursor) is untouched, correctly.

Verified in a browser: continuous mode pins the playhead at x=628, spread 0px
across a 6s pass; catch lands at exactly 0.30 of the USABLE width at both a
606px and a 1206px canvas (the width-invariance); the gutter divider paints at
x=52 and the cyan pin at centre x=629; reduced-motion reads false; no page
errors.

The follow policy fns are now exported and tested by real import (superseding
the sliced follow_toggle / loop_wrap_follow suites). tests/follow_scroll.test.mjs
pins the width-invariance (the behavioural discriminator — the old inline
formula lands at LABEL_W + viewW*0.3, which varies with width), continuous
centring, the shared pin fraction, the loop-wrap recentre, and the tick wiring.
tests/menu_model.test.mjs covers the checkbox checked/dimmed states.

228 JS tests, lint clean. routes.py untouched (no pytest change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xGPjDBF8NTwTK7VQvizix
ChrisBeWithYou added a commit that referenced this pull request Jul 19, 2026
…ll in Play (#331)

* feat(editor): follow respects the canvas edge, and gains Logic's Scroll in Play

Two things, from a look at how Logic/Ableton/Pro Tools handle the playhead
running off-screen during playback.

THE BUG (Christian: "doesn't respect the edge at different resolutions").
_followScrollTargetPure measured its 80% trigger and 30% landing against the
FULL canvas width, but read the cursor x from timeToX — which adds the 52px
LABEL_W gutter. So the usable timeline (viewW - 52) was never what it computed
against, and the effective landing fraction drifted with canvas width: 52px is
a bigger slice of an 800px canvas than a 1920px one. Real, but modest — a
constant ~16px, ~1-2% across resolutions, not a gross failure. Both the forward
follow and the loop-wrap recentre now measure against the usable width, so the
playhead lands at the same on-screen spot at every resolution.

Two things the DAW survey settled, worth recording:
  - The DPR half of the hypothesis was WRONG for this path — both cursorX and
    viewW are already CSS px (main.js scales the context by DPR). Adding a DPR
    term here would REINTRODUCE a bug (that was #320's defect, in input.js).
    Deliberately no DPR term.
  - The dramatic "ignores the edge at some window sizes" symptom is most likely
    a STALE canvas width, which #328 (the wrap ResizeObserver) fixes — not this
    formula. This PR is the gutter-math polish; #328 is the other half.

THE FEATURE. Follow now has two manners, matching Logic: page-jump (default,
unchanged) and continuous "Scroll in Play" — the playhead pins at centre and
the timeline glides under it. All three DAWs converge on exactly this page-vs-
continuous choice and offer nothing more, so it's the whole customization worth
having; Logic's full Catch matrix would be over-engineering for one window.

Surfaced as a View-menu checkbox (not a second button, not a 3-state cycle —
the Follow button's aria-pressed has no honest third value), dimmed while
Follow is off, honouring prefers-reduced-motion by falling back to the page
jump. The Follow button's tooltip and the status line name the live manner.

The label-gutter edge is now a faint divider in the chart, so the usable
timeline reads as bounded and a future gutter slip is visible on sight. The
continuous pin is drawn from _followPinXPure with the SAME fraction the scroll
target uses, so the pin can never point where the scroll doesn't hold.

Follow is global (moves S.scrollX), so one fix covers every time-axis view;
Tab view (alphaTab owns its own cursor) is untouched, correctly.

Verified in a browser: continuous mode pins the playhead at x=628, spread 0px
across a 6s pass; catch lands at exactly 0.30 of the USABLE width at both a
606px and a 1206px canvas (the width-invariance); the gutter divider paints at
x=52 and the cyan pin at centre x=629; reduced-motion reads false; no page
errors.

The follow policy fns are now exported and tested by real import (superseding
the sliced follow_toggle / loop_wrap_follow suites). tests/follow_scroll.test.mjs
pins the width-invariance (the behavioural discriminator — the old inline
formula lands at LABEL_W + viewW*0.3, which varies with width), continuous
centring, the shared pin fraction, the loop-wrap recentre, and the tick wiring.
tests/menu_model.test.mjs covers the checkbox checked/dimmed states.

228 JS tests, lint clean. routes.py untouched (no pytest change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xGPjDBF8NTwTK7VQvizix

* Describe the effective follow behavior

* Avoid stacked changelog conflicts

* Hide the follow pin when follow is off

* Normalize follow guide lines

---------

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant