Merge remote-tracking branch 'origin/master' into claude/issue-57

# Conflicts:
#	project.cpp
This commit is contained in:
catbot 2026-06-16 17:02:48 +00:00
commit ce1eaf6366
11 changed files with 470 additions and 25 deletions

View file

@ -87,6 +87,28 @@ std::uint32_t Font::GetLineWidth(const std::string_view text, float size) {
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;
cumWidth += (int)(AdvanceUnits(cp) * 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;

View file

@ -117,23 +117,12 @@ std::size_t Crafter::InputField_HitTestCursor(const InputField& f,
Font& font, float fontSize,
const InputFieldColors& colors)
{
// Single O(n) cumulative-advance pass over the value, keyed at codepoint
// byte boundaries — matching cursorPos, which is a byte offset. The old
// loop re-walked the prefix for every boundary (O(n^2) glyph metric
// lookups) and could even land mid-codepoint on multibyte input.
float target = clickX - (rect.x + colors.paddingX);
if (target <= 0.0f) return 0;
std::size_t best = 0;
float bestDist = std::abs(target);
for (std::size_t i = 1; i <= f.value.size(); ++i) {
std::string_view sub(f.value.data(), i);
float w = static_cast<float>(font.GetLineWidth(sub, fontSize));
float d = std::abs(target - w);
if (d < bestDist) {
bestDist = d;
best = i;
} else {
break;
}
}
return best;
return font.NearestCursorByte(f.value, fontSize, target);
}
void Crafter::DrawInputField(UIBuffer& buf, const InputField& f, Rect rect,

View file

@ -38,11 +38,27 @@ UIDispatchHeader UIRenderer::FillHeader(std::uint32_t itemBufferSlot,
#else
h.frameIdx = 0;
#endif
h.flags = flags;
h.flags = ClipFlags(clipRectPx, window_->width, window_->height, flags);
h._pad = 0;
return h;
}
std::uint32_t UIRenderer::ClipFlags(std::array<float,4> clipRectPx,
std::uint32_t surfaceWidth,
std::uint32_t surfaceHeight,
std::uint32_t flags) noexcept {
// The reserved kUIFlagClip bit signals "clip rect is narrower than the
// surface". When the rect already covers the whole surface (the common
// {0,0,1e9,1e9} default), the four per-pixel clip compares in
// uiResolveScreenPixel can never reject an in-surface pixel, so we leave
// the bit clear and the shader skips them — pixel-identical output.
const bool clipCoversSurface =
clipRectPx[0] <= 0.0f && clipRectPx[1] <= 0.0f
&& clipRectPx[0] + clipRectPx[2] >= static_cast<float>(surfaceWidth)
&& clipRectPx[1] + clipRectPx[3] >= static_cast<float>(surfaceHeight);
return clipCoversSurface ? (flags & ~kUIFlagClip) : (flags | kUIFlagClip);
}
std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
float x, float baselineY,
std::string_view utf8,