Skip to content

Fix arrow up/down and click pixel-to-position accuracy#106

Merged
hackerwins merged 6 commits into
mainfrom
fix/arrow-pixel-accuracy
Apr 5, 2026
Merged

Fix arrow up/down and click pixel-to-position accuracy#106
hackerwins merged 6 commits into
mainfrom
fix/arrow-pixel-accuracy

Conversation

@hackerwins

@hackerwins hackerwins commented Apr 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Pre-compute per-character pixel offsets (charOffsets) in LayoutRun during layout
  • Replace uniform character width approximation in paginatedPixelToPosition with binary search on charOffsets
  • Use charOffsets in getPixelForPosition for forward/reverse consistency

Problem

Arrow Up/Down and mouse clicks land on the wrong character with proportional fonts. The reverse mapping (paginatedPixelToPosition) used run.width / text.length as a uniform character width, while the forward mapping (getPixelForPosition) used ctx.measureText(). This asymmetry caused off-by-one cursor positioning.

Approach

Option chosen: Pre-compute charOffsets in layout (Option 2 from task analysis)

  • Added charOffsets: number[] to LayoutRun — cumulative pixel widths per character
  • Computed via computeCharOffsets() helper using ctx.measureText() during layout build
  • Binary search in paginatedPixelToPosition snaps to nearest character boundary
  • getPixelForPosition now reads from charOffsets instead of measuring at runtime

Test plan

  • Added 4 proportional-width hit detection tests in pagination.test.ts
  • All 345 docs tests pass
  • pnpm verify:fast passes (lint + unit tests)
  • Manual verification: arrow up/down, mouse click, selection drag all position correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Enhanced cursor positioning accuracy when clicking in text or navigating with arrow keys by implementing character-level offset precomputation instead of uniform-width assumptions.
  • Tests

    • Added comprehensive test coverage for character-level cursor positioning across edge cases and proportional character widths.

hackerwins and others added 4 commits April 5, 2026 16:15
Pre-compute cumulative character pixel widths during layout so that
paginatedPixelToPosition can use binary search instead of uniform
character width approximation.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Replace uniform character width approximation with binary search on
pre-computed per-character pixel offsets. Fixes Arrow Up/Down and
mouse click landing on wrong character with proportional fonts.
Test that paginatedPixelToPosition correctly snaps to characters
using pre-computed charOffsets with variable character widths.
Replace runtime ctx.measureText call with charOffsets lookup for
position-to-pixel mapping, ensuring forward and reverse conversions
use the same data source.
@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5432fab-b815-4f63-9f9b-adb29310b698

📥 Commits

Reviewing files that changed from the base of the PR and between e178aed and a53a59c.

📒 Files selected for processing (4)
  • docs/tasks/README.md
  • docs/tasks/active/20260325-docs-arrow-pixel-accuracy-plan.md
  • docs/tasks/archive/2026/03/20260325-docs-arrow-pixel-accuracy-todo.md
  • docs/tasks/archive/README.md

📝 Walkthrough

Walkthrough

This PR implements precomputed per-character offset tracking for text runs in the docs renderer. It adds a charOffsets array to LayoutRun, precomputes cumulative pixel widths during layout, and refactors character positioning logic in pagination and text-editor to use these offsets instead of runtime measurements.

Changes

Cohort / File(s) Summary
Layout computation
packages/docs/src/view/layout.ts
Added charOffsets: number[] to LayoutRun interface. Introduced computeCharOffsets() helper to precompute per-character cumulative widths. Extended MeasuredSegment to carry computed font string. Updated layoutBlock to populate charOffsets for all run construction paths.
Hit-testing refactor
packages/docs/src/view/pagination.ts, packages/docs/src/view/text-editor.ts
Replaced uniform-width character approximations with binary search over precomputed charOffsets in pagination hit-testing. Updated text-editor's getPixelForPosition to derive pixel offsets directly from charOffsets instead of runtime measureText calls, removing superscript/subscript font-size adjustment logic.
Table layout integration
packages/docs/src/view/table-layout.ts
Added charOffsets computation for inline word runs using the new computeCharOffsets helper.
Test coverage
packages/docs/test/view/pagination.test.ts, packages/docs/test/view/peer-cursor.test.ts
Added test helpers (mockRun, mockLineWithRuns) and comprehensive test cases for charOffsets-based character boundary snapping. Extended fixture data to include precomputed offsets.
Task tracking
docs/tasks/README.md, docs/tasks/active/20260325-docs-arrow-pixel-accuracy-plan.md, docs/tasks/archive/README.md, docs/tasks/archive/2026/03/20260325-docs-arrow-pixel-accuracy-todo.md
Archived completed "docs arrow pixel accuracy" task. Updated task counts. Documented implementation plan in active tasks directory.

Sequence Diagram(s)

sequenceDiagram
    participant Layout as Layout Engine
    participant CharOffset as Character Offsets
    participant Pagination as Pagination Hit-Test
    participant TextEditor as Text Editor

    rect rgba(200, 150, 255, 0.5)
    Note over Layout: Old Flow: Runtime Measurement
    Layout->>Pagination: Each run without precomputed offsets
    Pagination->>TextEditor: Per-substring measureText needed
    TextEditor->>Pagination: Computed width for character position
    end

    rect rgba(150, 200, 255, 0.5)
    Note over Layout: New Flow: Precomputed Offsets
    Layout->>CharOffset: Compute charOffsets per run
    CharOffset->>CharOffset: Canvas.measureText cumulative widths
    CharOffset->>Layout: Return offsets array
    Layout->>Pagination: Run with charOffsets
    Pagination->>Pagination: Binary search over charOffsets
    Pagination->>TextEditor: Character boundary located
    TextEditor->>TextEditor: Direct array lookup: charOffsets[index]
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

  • PR #59: Modifies the same pagination/layout/text-editor code paths for position-to-character mapping, directly building on shared infrastructure.
  • PR #92: Extends table-layout inline run construction where charOffsets computation is now integrated for word-level runs.
  • PR #64: Updates getPixelForPosition in text-editor.ts with overlapping concerns around run width handling and pixel position calculation.

Poem

🐰 A rabbit hops through measured pixels true,
Pre-baking offsets in each character's queue,
No more runtime math for every click—
Binary search makes positioning quick! ✨
Accurate arrows, cursor placement divine,
Character by character, the positions align! 📍

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix arrow up/down and click pixel-to-position accuracy' clearly and specifically summarizes the main change: improving pixel-to-character position mapping accuracy for both arrow navigation and mouse click interactions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/arrow-pixel-accuracy

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 and usage tips.

@github-actions

github-actions Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 109.4s

Lane Status Duration
sheets:build ✅ pass 12.8s
docs:build ✅ pass 6.5s
verify:fast ✅ pass 55.0s
frontend:build ✅ pass 14.9s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.3s
cli:build ✅ pass 1.8s
verify:entropy ✅ pass 13.7s

Verification: verify:integration

Result: ✅ PASS

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/docs/test/view/pagination.test.ts (1)

212-278: Add one round-trip regression for the forward path too.

These cases pin paginatedPixelToPosition(), but the original bug was the disagreement between reverse mapping and TextEditor.getPixelForPosition(). One public click→cursor→ArrowUp/Down or resolvePositionPixel() round-trip would protect the forward side as well.

As per coding guidelines, "**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases`."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/test/view/pagination.test.ts` around lines 212 - 278, Add a
forward-path round-trip test that after computing a position with
paginatedPixelToPosition you convert that position back to pixels and then
resolve pixels back to a position to ensure consistency: use the existing test
helpers (mockRun, mockLineWithRuns, mockBlock, paginateLayout) to create a small
run/line, call paginatedPixelToPosition to get pos, then call
TextEditor.getPixelForPosition (or resolvePositionPixel/getPixelForPosition
equivalent in this codebase) with that position to get pixelX and finally call
paginatedPixelToPosition again with pixelX and assert the final offset equals
the original offset; place this new it(...) in the same describe block so it
guards the forward mapping as well as the reverse.
packages/docs/src/view/table-layout.ts (1)

4-5: Unify table-cell hit testing around charOffsets.

Table runs now carry the same per-character metadata as top-level runs, but TextEditor.resolveOffsetInCellAtXY() still maps table-cell clicks by remeasuring text in a separate loop. Keeping both implementations around makes tables the next place this cursor drift can come back.

Also applies to: 95-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/table-layout.ts` around lines 4 - 5,
TextEditor.resolveOffsetInCellAtXY() currently remeasures text in a separate
loop instead of using the per-character metadata already present on table runs
(charOffsets), causing inconsistent hit testing; replace the manual
remeasurement with the same mapping used for top-level runs by reading
run.charOffsets (or calling computeCharOffsets(...) where missing), applying
applyAlignment(...) to adjust x for the run, and binary-searching or iterating
charOffsets to find the clicked character index, removing the duplicate
cachedMeasureText/remeasure loop and ensuring the resolved offset uses the run's
existing charOffsets and alignment logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/docs/src/view/table-layout.ts`:
- Around line 4-5: TextEditor.resolveOffsetInCellAtXY() currently remeasures
text in a separate loop instead of using the per-character metadata already
present on table runs (charOffsets), causing inconsistent hit testing; replace
the manual remeasurement with the same mapping used for top-level runs by
reading run.charOffsets (or calling computeCharOffsets(...) where missing),
applying applyAlignment(...) to adjust x for the run, and binary-searching or
iterating charOffsets to find the clicked character index, removing the
duplicate cachedMeasureText/remeasure loop and ensuring the resolved offset uses
the run's existing charOffsets and alignment logic.

In `@packages/docs/test/view/pagination.test.ts`:
- Around line 212-278: Add a forward-path round-trip test that after computing a
position with paginatedPixelToPosition you convert that position back to pixels
and then resolve pixels back to a position to ensure consistency: use the
existing test helpers (mockRun, mockLineWithRuns, mockBlock, paginateLayout) to
create a small run/line, call paginatedPixelToPosition to get pos, then call
TextEditor.getPixelForPosition (or resolvePositionPixel/getPixelForPosition
equivalent in this codebase) with that position to get pixelX and finally call
paginatedPixelToPosition again with pixelX and assert the final offset equals
the original offset; place this new it(...) in the same describe block so it
guards the forward mapping as well as the reverse.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b9cf184-e8d4-4ed2-9622-cba86c4dabe7

📥 Commits

Reviewing files that changed from the base of the PR and between 08e69f5 and 1151a05.

📒 Files selected for processing (6)
  • packages/docs/src/view/layout.ts
  • packages/docs/src/view/pagination.ts
  • packages/docs/src/view/table-layout.ts
  • packages/docs/src/view/text-editor.ts
  • packages/docs/test/view/pagination.test.ts
  • packages/docs/test/view/peer-cursor.test.ts

@hackerwins
hackerwins merged commit 4dfe015 into main Apr 5, 2026
1 check passed
@hackerwins
hackerwins deleted the fix/arrow-pixel-accuracy branch April 5, 2026 10:43
hackerwins added a commit that referenced this pull request Apr 8, 2026
The task was completed in PR #106 but the plan file remained in
active/. Move it to archive/2026/03/ alongside its todo file.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
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