Crafter.Graphics/interfaces/Crafter.Graphics-Font.cppm
catbot d712218f17 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>
2026-06-16 16:55:03 +00:00

76 lines
3 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;
#include "../lib/stb_truetype.h"
export module Crafter.Graphics:Font;
import std;
namespace Crafter {
// Decode the UTF-8 codepoint at `text[i]` and advance `i` past it.
// Returns 0 once `i` reaches the end. Malformed sequences yield U+FFFD
// and the index is moved past one byte to keep iteration finite.
export inline std::uint32_t DecodeUtf8(std::string_view text, std::size_t& i) {
if (i >= text.size()) return 0;
std::uint8_t b0 = static_cast<std::uint8_t>(text[i]);
if (b0 < 0x80) { ++i; return b0; }
int extra;
std::uint32_t cp;
if ((b0 & 0xE0) == 0xC0) { extra = 1; cp = b0 & 0x1F; }
else if ((b0 & 0xF0) == 0xE0) { extra = 2; cp = b0 & 0x0F; }
else if ((b0 & 0xF8) == 0xF0) { extra = 3; cp = b0 & 0x07; }
else { ++i; return 0xFFFD; }
++i;
for (int k = 0; k < extra; ++k) {
if (i >= text.size()) return 0xFFFD;
std::uint8_t b = static_cast<std::uint8_t>(text[i]);
if ((b & 0xC0) != 0x80) return 0xFFFD;
cp = (cp << 6) | (b & 0x3Fu);
++i;
}
return cp;
}
export class Font {
public:
std::vector<unsigned char> fontBuffer;
std::int_fast32_t ascent;
std::int_fast32_t descent;
std::int_fast32_t lineGap;
stbtt_fontinfo font;
Font(const std::filesystem::path& font);
std::uint32_t GetLineWidth(const std::string_view text, float size);
// Map a target advance (px from the line start) to the nearest UTF-8
// codepoint boundary, returning its BYTE offset into `text`. A single
// left-to-right pass accumulates per-glyph advances (O(n) metric
// lookups), instead of re-walking the prefix for every boundary as a
// naive caller would. Cumulative advance is monotonic (advances are
// non-negative), so the scan stops at the first boundary past the
// closest one. Returned offsets land on codepoint boundaries, which are
// exactly the valid positions for a byte-based text cursor.
std::size_t NearestCursorByte(std::string_view text, float size, float target);
float LineHeight(float size);
float AscentPx(float size);
float ScaleForSize(float size);
};
}