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>
110 lines
No EOL
3.7 KiB
C++
110 lines
No EOL
3.7 KiB
C++
/*
|
|
Crafter®.Graphics
|
|
Copyright (C) 2026 Catcrafts®
|
|
Catcrafts.net
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 3.0 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
module;
|
|
#define STB_TRUETYPE_IMPLEMENTATION
|
|
#include "../lib/stb_truetype.h"
|
|
module Crafter.Graphics:Font_impl;
|
|
import :Font;
|
|
import std;
|
|
|
|
using namespace Crafter;
|
|
|
|
Font::Font(const std::filesystem::path& fontFilePath) {
|
|
// 1. Load the font file into memory
|
|
std::ifstream fontFile(fontFilePath, std::ios::binary | std::ios::ate);
|
|
if (!fontFile.is_open()) {
|
|
std::cerr << "Failed to open font file\n";
|
|
}
|
|
|
|
std::streamsize size = fontFile.tellg();
|
|
fontFile.seekg(0, std::ios::beg);
|
|
fontBuffer.resize(size);
|
|
if (!fontFile.read((char*)fontBuffer.data(), size)) {
|
|
std::cerr << "Failed to read font file\n";
|
|
}
|
|
|
|
// 2. Initialize the font
|
|
if (!stbtt_InitFont(&font, fontBuffer.data(), stbtt_GetFontOffsetForIndex(fontBuffer.data(), 0))) {
|
|
std::cout << "Test6" << std::endl;
|
|
std::cerr << "Failed to initialize font.\n";
|
|
}
|
|
|
|
int ascent;
|
|
int descent;
|
|
int lineGap;
|
|
|
|
stbtt_GetFontVMetrics(&font, &ascent, &descent, &lineGap);
|
|
|
|
this->ascent = ascent;
|
|
this->descent = descent;
|
|
this->lineGap = lineGap;
|
|
}
|
|
|
|
std::uint32_t Font::GetLineWidth(const std::string_view text, float size) {
|
|
float scale = stbtt_ScaleForPixelHeight(&font, size);
|
|
std::uint32_t lineWidth = 0;
|
|
std::size_t i = 0;
|
|
while (i < text.size()) {
|
|
std::uint32_t cp = DecodeUtf8(text, i);
|
|
if (cp == 0) break;
|
|
int advance, lsb;
|
|
stbtt_GetCodepointHMetrics(&font, static_cast<int>(cp), &advance, &lsb);
|
|
lineWidth += (int)(advance * scale);
|
|
}
|
|
return lineWidth;
|
|
}
|
|
|
|
std::size_t Font::NearestCursorByte(std::string_view text, float size, float target) {
|
|
if (target <= 0.0f) return 0;
|
|
float scale = stbtt_ScaleForPixelHeight(&font, size);
|
|
std::size_t best = 0; // byte offset 0 → caret before the first glyph
|
|
float bestDist = target; // |target - 0|, and target > 0 here
|
|
float cumWidth = 0.0f;
|
|
std::size_t i = 0;
|
|
while (i < text.size()) {
|
|
std::uint32_t cp = DecodeUtf8(text, i); // i advances to the next boundary
|
|
if (cp == 0) break;
|
|
int advance, lsb;
|
|
stbtt_GetCodepointHMetrics(&font, static_cast<int>(cp), &advance, &lsb);
|
|
cumWidth += (int)(advance * scale); // match GetLineWidth's per-glyph rounding
|
|
float d = std::abs(target - cumWidth);
|
|
if (d < bestDist) {
|
|
bestDist = d;
|
|
best = i; // byte offset just past this codepoint
|
|
} else {
|
|
break; // monotonic advance ⇒ already past the closest boundary
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
float Font::LineHeight(float size) {
|
|
float scale = stbtt_ScaleForPixelHeight(&font, size);
|
|
return (ascent - descent + lineGap) * scale;
|
|
}
|
|
|
|
float Font::AscentPx(float size) {
|
|
return ascent * stbtt_ScaleForPixelHeight(&font, size);
|
|
}
|
|
|
|
float Font::ScaleForSize(float size) {
|
|
return stbtt_ScaleForPixelHeight(&font, size);
|
|
} |