Crafter.Graphics/interfaces/Crafter.Graphics-InputField.cppm

108 lines
5 KiB
Text
Raw Normal View History

2026-07-22 18:09:06 +02:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
2026-05-03 02:45:38 +02:00
module;
2026-05-18 02:07:48 +02:00
2026-05-03 02:45:38 +02:00
export module Crafter.Graphics:InputField;
import std;
import :Types;
2026-05-12 00:24:48 +02:00
import :Keys;
2026-05-03 02:45:38 +02:00
import :Font;
import :UI;
import :UIComponents;
// Tier 3: single-line text input. Same contract as the other UI components —
// state is a user-owned POD, the draw function is stateless presentation, and
// input feeding is done via free functions called from the host's own event
// listeners. Validation rules cover the common numeric cases (UInt/Int/Float)
// and accept "in-progress" intermediate states (lone '-', '.', "1e") so the
// user can type naturally; final-form parsing is on the caller via TryParse.
export namespace Crafter {
enum class InputFieldType : std::uint8_t {
Text, // any UTF-8
UInt, // digits only
Int, // optional leading '-', then digits
Float, // signed float with optional exponent
};
struct InputFieldColors {
std::array<float, 4> bg;
std::array<float, 4> bgFocused;
std::array<float, 4> border;
std::array<float, 4> borderFocused;
std::array<float, 4> text;
std::array<float, 4> caret;
float cornerRadius = 6.0f;
float borderThickness = 2.0f;
float paddingX = 8.0f; // text inset from the rect's left edge
};
struct InputField {
std::string value;
InputFieldType type = InputFieldType::Text;
bool focused = false;
std::size_t cursorPos = 0; // byte offset into `value`
// Caret-x memo (issue #128). For a focused field DrawInputField measures
// the cursor prefix via Font::GetLineWidth every frame, but only the
// blink (caretVisible) changes frame-to-frame — value/cursorPos/fontSize
// are stable across the vast majority of frames. Cache the measured
// prefix WIDTH (the GetLineWidth result, in px) keyed on the inputs that
// determine it: the prefix bytes and fontSize. The final caretX adds the
// layout-dependent textX each frame, so it is intentionally NOT cached —
// a field that moves on screen with an unchanged value would otherwise
// get a stale caret. `mutable` so the `const InputField&` draw fn can
// refresh it; these fields are internal and need no user initialisation.
mutable std::string caretCachePrefix_;
mutable float caretCacheFontSize_ = -1.0f; // <0 = unset
mutable float caretCacheWidth_ = 0.0f;
2026-05-03 02:45:38 +02:00
};
// Returns true if `s` is a valid candidate for the given type, including
// "in-progress" states (lone '-', empty, "1e", "1.").
bool InputField_IsValidCandidate(InputFieldType, std::string_view s);
// Try to parse the field's current value into T. Returns nullopt for empty
// or in-progress strings. Defined for arithmetic types via std::from_chars.
template <class T>
std::optional<T> InputField_TryParse(const InputField& f) {
if (f.value.empty()) return std::nullopt;
T out{};
const char* begin = f.value.data();
const char* end = begin + f.value.size();
auto [ptr, ec] = std::from_chars(begin, end, out);
if (ec != std::errc{} || ptr != end) return std::nullopt;
return out;
}
// Insert UTF-8 text at the cursor. The full would-be result is validated
// against the field's type before committing — invalid candidates are
// dropped silently. Cursor advances by the inserted byte count.
void InputField_OnText(InputField&, std::string_view utf8);
// Edit-control keys: Backspace, Delete, Left, Right, Home, End. Anything
2026-05-12 00:24:48 +02:00
// else is ignored. Safe to feed every key the host receives. The KeyCode
// is the raw platform key (Win32 PS/2 scancode + extended bit; Wayland
// kernel keycode) — typically the value delivered by
// `Window::onRawKeyDown`. Internal comparisons use `Key(CrafterKeys::X)`
// from :Keys so the code stays cross-platform.
void InputField_OnKey(InputField&, KeyCode);
2026-05-03 02:45:38 +02:00
// Map a click x-coord (in window pixels) to a cursor position. `rect` is
// the field's current draw rect; `colors.paddingX` is consulted for the
// text-start offset.
std::size_t InputField_HitTestCursor(const InputField&,
Rect rect,
float clickX,
Font&, float fontSize,
const InputFieldColors&);
// Draw the field. `caretVisible` is provided by the caller so that blink
// policy stays in user code (typical: `(steady_clock::now() / 500ms) & 1`).
// The caret is only drawn if the field is focused AND caretVisible is true.
void DrawInputField(UIBuffer& buf, const InputField& f, Rect rect,
Font& font, float fontSize,
const InputFieldColors& colors,
bool caretVisible);
}