perf(input-field): O(n) cursor hit test via Font::NearestCursorByte (#56)
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>
This commit is contained in:
parent
c58b68ec40
commit
d712218f17
6 changed files with 280 additions and 16 deletions
30
project.cpp
30
project.cpp
|
|
@ -413,6 +413,36 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
memImpls.emplace_back("tests/MemoryTypeFallback/main");
|
||||
mc.GetInterfacesAndImplementations(ifaces, memImpls);
|
||||
cfg.tests.push_back(std::move(memTest));
|
||||
|
||||
// Issue #56: InputField_HitTestCursor mapped a click x to a cursor byte
|
||||
// offset by re-walking the prefix for every boundary (O(n^2) glyph
|
||||
// metric lookups) over raw byte boundaries. It now delegates to
|
||||
// Font::NearestCursorByte — one cumulative-advance pass over codepoint
|
||||
// boundaries. The mapping is pure CPU over a TrueType file, so this test
|
||||
// drives it directly: no Vulkan device or window. The font is copied
|
||||
// next to the binary; the test also probes the project-root path.
|
||||
Test hitTest;
|
||||
Configuration& hc = hitTest.config;
|
||||
hc.path = cfg.path;
|
||||
hc.name = "InputFieldHitTest";
|
||||
hc.outputName = "InputFieldHitTest";
|
||||
hc.type = ConfigurationType::Executable;
|
||||
hc.target = cfg.target;
|
||||
hc.march = cfg.march;
|
||||
hc.mtune = cfg.mtune;
|
||||
hc.debug = cfg.debug;
|
||||
hc.sysroot = cfg.sysroot;
|
||||
hc.dependencies = cfg.dependencies;
|
||||
hc.externalDependencies = cfg.externalDependencies;
|
||||
hc.compileFlags = cfg.compileFlags;
|
||||
hc.linkFlags = cfg.linkFlags;
|
||||
hc.defines = cfg.defines;
|
||||
hc.cFiles = cfg.cFiles;
|
||||
hc.files = { fs::path("tests/InputFieldHitTest/font.ttf") };
|
||||
std::vector<fs::path> hitImpls(impls.begin(), impls.end());
|
||||
hitImpls.emplace_back("tests/InputFieldHitTest/main");
|
||||
hc.GetInterfacesAndImplementations(ifaces, hitImpls);
|
||||
cfg.tests.push_back(std::move(hitTest));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue