Crafter.Graphics/implementations/Crafter.Graphics-InputField.cpp

163 lines
6.1 KiB
C++

//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
module Crafter.Graphics:InputField_impl;
import :InputField;
import :UI;
import :UIComponents;
import :Font;
import :Types;
import :Keys;
import std;
using namespace Crafter;
bool Crafter::InputField_IsValidCandidate(InputFieldType type, std::string_view s) {
switch (type) {
case InputFieldType::Text:
return true;
case InputFieldType::UInt: {
for (char c : s) if (c < '0' || c > '9') return false;
return true;
}
case InputFieldType::Int: {
if (s.empty()) return true;
std::size_t i = 0;
if (s[0] == '-') {
if (s.size() == 1) return true; // lone '-' allowed mid-edit
i = 1;
}
for (; i < s.size(); ++i)
if (s[i] < '0' || s[i] > '9') return false;
return true;
}
case InputFieldType::Float: {
if (s.empty()) return true;
std::size_t i = 0;
if (s[0] == '-') {
if (s.size() == 1) return true;
i = 1;
}
bool sawDigit = false, sawDot = false, sawExp = false;
for (; i < s.size(); ++i) {
char c = s[i];
if (c >= '0' && c <= '9') {
if (!sawExp) sawDigit = true;
} else if (c == '.') {
if (sawDot || sawExp) return false;
sawDot = true;
} else if (c == 'e' || c == 'E') {
if (sawExp || !sawDigit) return false;
sawExp = true;
if (i + 1 < s.size() && (s[i + 1] == '+' || s[i + 1] == '-')) ++i;
} else {
return false;
}
}
return true;
}
}
return true;
}
void Crafter::InputField_OnText(InputField& f, std::string_view utf8) {
if (utf8.empty()) return;
std::string candidate = f.value;
candidate.insert(f.cursorPos, utf8);
if (!InputField_IsValidCandidate(f.type, candidate)) return;
f.value = std::move(candidate);
f.cursorPos += utf8.size();
}
void Crafter::InputField_OnKey(InputField& f, KeyCode code) {
// `Key(...)` is consteval — every comparison below folds to an immediate
// integer at compile time, so this is just a sequence of int-eq tests.
if (code == Key(CrafterKeys::Backspace)) {
if (f.cursorPos > 0 && !f.value.empty()) {
f.value.erase(f.cursorPos - 1, 1);
--f.cursorPos;
}
} else if (code == Key(CrafterKeys::Delete)) {
if (f.cursorPos < f.value.size()) {
f.value.erase(f.cursorPos, 1);
}
} else if (code == Key(CrafterKeys::Left)) {
if (f.cursorPos > 0) --f.cursorPos;
} else if (code == Key(CrafterKeys::Right)) {
if (f.cursorPos < f.value.size()) ++f.cursorPos;
} else if (code == Key(CrafterKeys::Home)) {
f.cursorPos = 0;
} else if (code == Key(CrafterKeys::End)) {
f.cursorPos = f.value.size();
}
}
std::size_t Crafter::InputField_HitTestCursor(const InputField& f,
Rect rect,
float clickX,
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);
return font.NearestCursorByte(f.value, fontSize, target);
}
void Crafter::DrawInputField(UIBuffer& buf, const InputField& f, Rect rect,
Font& font, float fontSize,
const InputFieldColors& c,
bool caretVisible)
{
if (buf.quads == nullptr || buf.quadCount == nullptr) return;
const auto& bg = f.focused ? c.bgFocused : c.bg;
const auto& border = f.focused ? c.borderFocused : c.border;
if (*buf.quadCount < buf.quadCap) {
buf.quads[(*buf.quadCount)++] = QuadItem{
rect.x, rect.y, rect.w, rect.h,
bg[0], bg[1], bg[2], bg[3],
c.cornerRadius, c.cornerRadius, c.cornerRadius, c.cornerRadius,
c.borderThickness, border[0], border[1], border[2],
};
}
float baseline = rect.y + rect.h * 0.5f + fontSize * 0.32f;
float textX = rect.x + c.paddingX;
DrawText(buf, f.value, textX, baseline, font, fontSize, c.text, TextAlign::Left);
if (f.focused && caretVisible && *buf.quadCount < buf.quadCap) {
std::string_view sub(f.value.data(), std::min(f.cursorPos, f.value.size()));
// Memoise the prefix width (issue #128): only the blink changes between
// frames, so re-walking the prefix every frame is wasted work. A byte
// compare of the cached prefix (plus fontSize) is far cheaper than the
// per-glyph UTF-8 decode + advance accumulation GetLineWidth does.
float width;
if (f.caretCacheFontSize_ == fontSize && f.caretCachePrefix_ == sub) {
width = f.caretCacheWidth_;
} else {
width = sub.empty() ? 0.0f
: static_cast<float>(font.GetLineWidth(sub, fontSize));
f.caretCachePrefix_.assign(sub);
f.caretCacheFontSize_ = fontSize;
f.caretCacheWidth_ = width;
}
float caretX = textX + width;
float caretH = fontSize * 1.1f;
float caretY = rect.y + (rect.h - caretH) * 0.5f;
float caretW = std::max(1.0f, fontSize / 16.0f);
buf.quads[(*buf.quadCount)++] = QuadItem{
caretX, caretY, caretW, caretH,
c.caret[0], c.caret[1], c.caret[2], c.caret[3],
0, 0, 0, 0,
0, 0, 0, 0,
};
}
}