perf(input-field): O(n) cursor hit test via Font::NearestCursorByte (#56) #90

Merged
catbot merged 1 commit from claude/issue-56 into master 2026-06-16 18:55:35 +02:00
Member

Summary

InputField_HitTestCursor mapped a click x-coord to a cursor byte offset by calling Font::GetLineWidth on every prefix value[0..i] for i = 1..size. Each call re-walks from byte 0 — O(n²) glyph-metric lookups (each an uncached stbtt_GetCodepointHMetrics cmap binary search). It also iterated raw byte boundaries, so on multibyte UTF-8 input it could return an offset that splits a codepoint, an invalid position for the byte-based cursorPos.

Fix

  • Add Font::NearestCursorByte(text, size, target): a single left-to-right cumulative-advance pass (O(n) metrics, scale computed once) over codepoint boundaries, returning the byte offset of the boundary whose cumulative advance is nearest target. Cumulative advance is monotonic (advances are non-negative), so the scan stops at the first boundary past the closest one — and offsets always land on codepoint boundaries, the valid cursor positions.
  • InputField_HitTestCursor now delegates to it.

Tests

New tests/InputFieldHitTest — pure CPU over a TrueType file, no Vulkan device or window:

  • a fine px sweep across ASCII and multibyte ("café naïve ✓") strings, compared against an independent brute-force nearest-boundary oracle (built from per-prefix GetLineWidth, no early break — so it also catches any over-eager break);
  • a codepoint-boundary invariant on every returned offset;
  • left-of-start / far-left / empty-field → 0, far-past-end → value.size(), and glyph-edge resolution.

crafter-build test8 passed (including the new test).

Resolves #56

🤖 Generated with Claude Code

## Summary `InputField_HitTestCursor` mapped a click x-coord to a cursor byte offset by calling `Font::GetLineWidth` on **every** prefix `value[0..i]` for `i = 1..size`. Each call re-walks from byte 0 — O(n²) glyph-metric lookups (each an uncached `stbtt_GetCodepointHMetrics` cmap binary search). It also iterated **raw byte** boundaries, so on multibyte UTF-8 input it could return an offset that splits a codepoint, an invalid position for the byte-based `cursorPos`. ## Fix - Add `Font::NearestCursorByte(text, size, target)`: a single left-to-right cumulative-advance pass (O(n) metrics, scale computed once) over **codepoint** boundaries, returning the byte offset of the boundary whose cumulative advance is nearest `target`. Cumulative advance is monotonic (advances are non-negative), so the scan stops at the first boundary past the closest one — and offsets always land on codepoint boundaries, the valid cursor positions. - `InputField_HitTestCursor` now delegates to it. ## Tests New `tests/InputFieldHitTest` — pure CPU over a TrueType file, no Vulkan device or window: - a fine px sweep across ASCII and multibyte ("café naïve ✓") strings, compared against an **independent** brute-force nearest-boundary oracle (built from per-prefix `GetLineWidth`, no early break — so it also catches any over-eager break); - a codepoint-boundary invariant on every returned offset; - left-of-start / far-left / empty-field → 0, far-past-end → `value.size()`, and glyph-edge resolution. `crafter-build test` → **8 passed** (including the new test). Resolves #56 🤖 Generated with [Claude Code](https://claude.com/claude-code)
InputField_HitTestCursor mapped a click x to a cursor byte offset by
calling Font::GetLineWidth on every prefix value[0..i] for i=1..size.
Each call re-walks from byte 0, so the hit test was O(n^2) glyph-metric
lookups (each an uncached stbtt cmap binary search). It also iterated
raw byte boundaries, so on multibyte UTF-8 input it could return an
offset splitting a codepoint — an invalid position for the byte-based
cursorPos.

Add Font::NearestCursorByte: one left-to-right cumulative-advance pass
(O(n) metrics, scale computed once) over codepoint boundaries, returning
the byte offset of the boundary nearest the target. Cumulative advance is
monotonic, so the scan stops past the closest boundary. The hit test now
delegates to it.

Adds tests/InputFieldHitTest, a pure-CPU regression test (no Vulkan
device/window): a px sweep across ASCII and multibyte strings compared
against an independent brute-force nearest-boundary oracle, plus the
left/right/empty/end-of-text edge cases and a codepoint-boundary invariant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit 79db987f83 into master 2026-06-16 18:55:35 +02:00
catbot deleted branch claude/issue-56 2026-06-16 18:55:35 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Catcrafts/Crafter.Graphics!90
No description provided.