update
This commit is contained in:
parent
1f5697326c
commit
c054f1e0b3
9 changed files with 699 additions and 5 deletions
BIN
examples/OptionsSpike/font.ttf
Normal file
BIN
examples/OptionsSpike/font.ttf
Normal file
Binary file not shown.
309
examples/OptionsSpike/main.cpp
Normal file
309
examples/OptionsSpike/main.cpp
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
// Spike for the OptionsMenu port (3DForts → Crafter.Graphics imperative UI).
|
||||||
|
// Validates: Rect::SubRect carving at real-menu density, InputField shape and
|
||||||
|
// caret blink, click-routing-to-focused-field, tab switching with per-tab
|
||||||
|
// builders. Throwaway example, but kept around as a living reference.
|
||||||
|
|
||||||
|
#include "vulkan/vulkan.h"
|
||||||
|
|
||||||
|
import Crafter.Graphics;
|
||||||
|
import Crafter.Event;
|
||||||
|
import std;
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum class Tab : std::uint8_t { Graphics, Input, Audio };
|
||||||
|
constexpr std::array<std::string_view, 3> kTabLabels = {
|
||||||
|
"Graphics", "Input", "Audio"
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Layout {
|
||||||
|
Rect canvas;
|
||||||
|
Rect titleBar;
|
||||||
|
Rect footer;
|
||||||
|
Rect main;
|
||||||
|
Rect tabStrip;
|
||||||
|
Rect content;
|
||||||
|
std::array<Rect, 3> tabRects;
|
||||||
|
Rect btnExit;
|
||||||
|
Rect btnSave;
|
||||||
|
};
|
||||||
|
|
||||||
|
Layout ComputeLayout(const Window& window) {
|
||||||
|
Layout L;
|
||||||
|
L.canvas = Rect::FromWindow(window);
|
||||||
|
L.titleBar = L.canvas.SubRect(60, Rect::Anchor::Top);
|
||||||
|
L.footer = L.canvas.SubRect(60, Rect::Anchor::Bottom);
|
||||||
|
L.main = L.canvas.Inset(60, 0, 60, 0);
|
||||||
|
L.tabStrip = L.main.SubRect(220, Rect::Anchor::Left).Inset(20);
|
||||||
|
L.content = L.main.Inset(0, 20, 0, 240);
|
||||||
|
|
||||||
|
Rect strip = L.tabStrip;
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
L.tabRects[i] = strip.SubRect(48, Rect::Anchor::Top).Inset(0, 0, 8, 0);
|
||||||
|
strip = strip.Inset(56, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect footerInset = L.footer.Inset(20, 60, 20, 60);
|
||||||
|
L.btnExit = footerInset.SubRect(160, Rect::Anchor::Left);
|
||||||
|
L.btnSave = footerInset.SubRect(160, Rect::Anchor::Right);
|
||||||
|
return L;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Device::Initialize();
|
||||||
|
Window window(1280, 720, "OptionsSpike");
|
||||||
|
|
||||||
|
VkCommandBuffer init = window.StartInit();
|
||||||
|
|
||||||
|
DescriptorHeapVulkan heap;
|
||||||
|
heap.Initialize(/*images*/ 16, /*buffers*/ 16, /*samplers*/ 4);
|
||||||
|
window.descriptorHeap = &heap;
|
||||||
|
|
||||||
|
Font font("font.ttf");
|
||||||
|
|
||||||
|
FontAtlas atlas;
|
||||||
|
atlas.Initialize(init);
|
||||||
|
|
||||||
|
UIRenderer ui;
|
||||||
|
ui.fontAtlas = &atlas;
|
||||||
|
ui.Initialize(window, heap, init);
|
||||||
|
window.passes.push_back(&ui);
|
||||||
|
|
||||||
|
VulkanBuffer<QuadItem, true> quadsBuf;
|
||||||
|
VulkanBuffer<GlyphItem, true> glyphsBuf;
|
||||||
|
quadsBuf.Create(
|
||||||
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
||||||
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 256);
|
||||||
|
glyphsBuf.Create(
|
||||||
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
||||||
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 4096);
|
||||||
|
|
||||||
|
auto quadsSlot = ui.RegisterBuffer(quadsBuf);
|
||||||
|
auto glyphsSlot = ui.RegisterBuffer(glyphsBuf);
|
||||||
|
|
||||||
|
// Application state.
|
||||||
|
Tab activeTab = Tab::Graphics;
|
||||||
|
std::array<bool, 3> tabHover{};
|
||||||
|
bool exitHover = false, saveHover = false;
|
||||||
|
|
||||||
|
// Graphics-tab fields. In the real port these are rebuilt per-tab; here we
|
||||||
|
// just keep one set live and only show them on the Graphics tab to confirm
|
||||||
|
// the routing pattern. Width/height come from a fake "tempOptions" state.
|
||||||
|
InputField widthField { .value = "1920", .type = InputFieldType::UInt };
|
||||||
|
InputField heightField { .value = "1080", .type = InputFieldType::UInt };
|
||||||
|
std::array<InputField*, 2> fields{ &widthField, &heightField };
|
||||||
|
std::array<Rect, 2> fieldRects{};
|
||||||
|
std::array<bool, 2> fieldHover{};
|
||||||
|
std::ptrdiff_t focusedField = -1;
|
||||||
|
|
||||||
|
auto BlurAll = [&]() {
|
||||||
|
for (auto* f : fields) f->focused = false;
|
||||||
|
focusedField = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto SetFocus = [&](std::ptrdiff_t idx) {
|
||||||
|
BlurAll();
|
||||||
|
if (idx >= 0 && idx < std::ssize(fields)) {
|
||||||
|
fields[idx]->focused = true;
|
||||||
|
focusedField = idx;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto LayoutGraphicsTabFields = [&](const Layout& L) {
|
||||||
|
Rect rows = L.content;
|
||||||
|
Rect row0 = rows.SubRect(48, Rect::Anchor::Top);
|
||||||
|
rows = rows.Inset(56, 0, 0, 0);
|
||||||
|
Rect row1 = rows.SubRect(48, Rect::Anchor::Top);
|
||||||
|
|
||||||
|
// Each row: label on left half, field on right half (right half inset).
|
||||||
|
fieldRects[0] = row0.SubRect(row0.w * 0.5f, Rect::Anchor::Right).Inset(4);
|
||||||
|
fieldRects[1] = row1.SubRect(row1.w * 0.5f, Rect::Anchor::Right).Inset(4);
|
||||||
|
};
|
||||||
|
|
||||||
|
Layout L = ComputeLayout(window);
|
||||||
|
LayoutGraphicsTabFields(L);
|
||||||
|
|
||||||
|
// ─── input listeners ───────────────────────────────────────────────
|
||||||
|
EventListener<void> moveSub(&window.onMouseMove, [&]() {
|
||||||
|
float mx = window.currentMousePos.x;
|
||||||
|
float my = window.currentMousePos.y;
|
||||||
|
for (int i = 0; i < 3; ++i) tabHover[i] = L.tabRects[i].Contains(mx, my);
|
||||||
|
exitHover = L.btnExit.Contains(mx, my);
|
||||||
|
saveHover = L.btnSave.Contains(mx, my);
|
||||||
|
if (activeTab == Tab::Graphics) {
|
||||||
|
for (int i = 0; i < (int)fields.size(); ++i)
|
||||||
|
fieldHover[i] = fieldRects[i].Contains(mx, my);
|
||||||
|
} else {
|
||||||
|
for (auto& h : fieldHover) h = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
EventListener<void> clickSub(&window.onMouseLeftClick, [&]() {
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
if (tabHover[i]) {
|
||||||
|
activeTab = static_cast<Tab>(i);
|
||||||
|
BlurAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exitHover) { window.open = false; return; }
|
||||||
|
if (saveHover) {
|
||||||
|
// In the real port: OptionsIO::Save + Apply. Here we just print.
|
||||||
|
std::println("[spike] save: width={} height={}",
|
||||||
|
widthField.value, heightField.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeTab == Tab::Graphics) {
|
||||||
|
for (int i = 0; i < (int)fields.size(); ++i) {
|
||||||
|
if (fieldHover[i]) {
|
||||||
|
SetFocus(i);
|
||||||
|
fields[i]->cursorPos = InputField_HitTestCursor(
|
||||||
|
*fields[i], fieldRects[i],
|
||||||
|
window.currentMousePos.x,
|
||||||
|
font, 18.0f, InputFieldColors{
|
||||||
|
.bg = {0.18f, 0.18f, 0.22f, 1.0f},
|
||||||
|
.bgFocused = {0.22f, 0.22f, 0.30f, 1.0f},
|
||||||
|
.border = {0.10f, 0.10f, 0.14f, 1.0f},
|
||||||
|
.borderFocused = {0.40f, 0.65f, 1.00f, 1.0f},
|
||||||
|
.text = {1.00f, 1.00f, 1.00f, 1.0f},
|
||||||
|
.caret = {1.00f, 1.00f, 1.00f, 1.0f},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BlurAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
EventListener<const std::string_view> textSub(&window.onTextInput, [&](std::string_view t) {
|
||||||
|
if (focusedField >= 0) InputField_OnText(*fields[focusedField], t);
|
||||||
|
});
|
||||||
|
EventListener<CrafterKeys> keySub(&window.onAnyKeyDown, [&](CrafterKeys k) {
|
||||||
|
if (focusedField >= 0) InputField_OnKey(*fields[focusedField], k);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── palettes ──────────────────────────────────────────────────────
|
||||||
|
ButtonColors tabPalette{
|
||||||
|
.bg = {0.20f, 0.22f, 0.28f, 1.0f},
|
||||||
|
.bgHover = {0.30f, 0.55f, 0.95f, 1.0f},
|
||||||
|
.bgPressed = {0.18f, 0.40f, 0.78f, 1.0f},
|
||||||
|
.text = {1, 1, 1, 1},
|
||||||
|
.cornerRadius = 6.0f,
|
||||||
|
};
|
||||||
|
ButtonColors tabPaletteSelected = tabPalette;
|
||||||
|
tabPaletteSelected.bg = {0.30f, 0.55f, 0.95f, 1.0f};
|
||||||
|
|
||||||
|
ButtonColors footerPalette{
|
||||||
|
.bg = {0.20f, 0.22f, 0.28f, 1.0f},
|
||||||
|
.bgHover = {0.30f, 0.55f, 0.95f, 1.0f},
|
||||||
|
.bgPressed = {0.18f, 0.40f, 0.78f, 1.0f},
|
||||||
|
.text = {1, 1, 1, 1},
|
||||||
|
.cornerRadius = 8.0f,
|
||||||
|
};
|
||||||
|
|
||||||
|
InputFieldColors fieldPalette{
|
||||||
|
.bg = {0.18f, 0.18f, 0.22f, 1.0f},
|
||||||
|
.bgFocused = {0.22f, 0.22f, 0.30f, 1.0f},
|
||||||
|
.border = {0.10f, 0.10f, 0.14f, 1.0f},
|
||||||
|
.borderFocused = {0.40f, 0.65f, 1.00f, 1.0f},
|
||||||
|
.text = {1, 1, 1, 1},
|
||||||
|
.caret = {1, 1, 1, 1},
|
||||||
|
};
|
||||||
|
|
||||||
|
auto startTime = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
EventListener<UIBuildArgs> buildSub(&ui.onBuild, [&](UIBuildArgs a) {
|
||||||
|
VkCommandBuffer cmd = a.cmd;
|
||||||
|
|
||||||
|
L = ComputeLayout(window);
|
||||||
|
LayoutGraphicsTabFields(L);
|
||||||
|
|
||||||
|
std::uint32_t qc = 0, gc = 0;
|
||||||
|
UIBuffer buf{
|
||||||
|
.quads = quadsBuf.value,
|
||||||
|
.quadCount = &qc,
|
||||||
|
.quadCap = 256,
|
||||||
|
.glyphs = glyphsBuf.value,
|
||||||
|
.glyphCount = &gc,
|
||||||
|
.glyphCap = 4096,
|
||||||
|
.atlas = &atlas,
|
||||||
|
.renderer = &ui,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Background.
|
||||||
|
if (qc < buf.quadCap) buf.quads[qc++] = QuadItem{
|
||||||
|
L.canvas.x, L.canvas.y, L.canvas.w, L.canvas.h,
|
||||||
|
0.10f, 0.10f, 0.13f, 1.0f,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
};
|
||||||
|
// Title bar.
|
||||||
|
if (qc < buf.quadCap) buf.quads[qc++] = QuadItem{
|
||||||
|
L.titleBar.x, L.titleBar.y, L.titleBar.w, L.titleBar.h,
|
||||||
|
0.14f, 0.14f, 0.18f, 1.0f,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
};
|
||||||
|
DrawText(buf, "OPTIONS",
|
||||||
|
L.titleBar.x + L.titleBar.w * 0.5f,
|
||||||
|
L.titleBar.y + L.titleBar.h * 0.5f + 18.0f * 0.32f,
|
||||||
|
font, 24.0f, {1, 1, 1, 1}, TextAlign::Center);
|
||||||
|
|
||||||
|
// Tabs.
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
const auto& palette = (activeTab == static_cast<Tab>(i))
|
||||||
|
? tabPaletteSelected : tabPalette;
|
||||||
|
DrawButton(buf, L.tabRects[i], kTabLabels[i],
|
||||||
|
tabHover[i], false, font, 18.0f, palette);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active tab content.
|
||||||
|
if (activeTab == Tab::Graphics) {
|
||||||
|
Rect rows = L.content;
|
||||||
|
Rect row0 = rows.SubRect(48, Rect::Anchor::Top);
|
||||||
|
rows = rows.Inset(56, 0, 0, 0);
|
||||||
|
Rect row1 = rows.SubRect(48, Rect::Anchor::Top);
|
||||||
|
|
||||||
|
DrawText(buf, "Resolution width",
|
||||||
|
row0.x + 8.0f,
|
||||||
|
row0.y + row0.h * 0.5f + 18.0f * 0.32f,
|
||||||
|
font, 18.0f, {0.85f, 0.85f, 0.85f, 1.0f}, TextAlign::Left);
|
||||||
|
DrawText(buf, "Resolution height",
|
||||||
|
row1.x + 8.0f,
|
||||||
|
row1.y + row1.h * 0.5f + 18.0f * 0.32f,
|
||||||
|
font, 18.0f, {0.85f, 0.85f, 0.85f, 1.0f}, TextAlign::Left);
|
||||||
|
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - startTime).count();
|
||||||
|
bool caretVisible = ((ms / 500) & 1) == 0;
|
||||||
|
|
||||||
|
DrawInputField(buf, widthField, fieldRects[0], font, 18.0f, fieldPalette, caretVisible);
|
||||||
|
DrawInputField(buf, heightField, fieldRects[1], font, 18.0f, fieldPalette, caretVisible);
|
||||||
|
} else {
|
||||||
|
DrawText(buf, "(tab content not populated in spike)",
|
||||||
|
L.content.x + L.content.w * 0.5f,
|
||||||
|
L.content.y + L.content.h * 0.5f,
|
||||||
|
font, 18.0f, {0.6f, 0.6f, 0.6f, 1.0f}, TextAlign::Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer.
|
||||||
|
DrawButton(buf, L.btnExit, "Exit", exitHover, false, font, 18.0f, footerPalette);
|
||||||
|
DrawButton(buf, L.btnSave, "Save changes", saveHover, false, font, 18.0f, footerPalette);
|
||||||
|
|
||||||
|
// Dispatch.
|
||||||
|
if (qc > 0) {
|
||||||
|
quadsBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||||
|
ui.DispatchQuads(cmd, quadsSlot, qc);
|
||||||
|
}
|
||||||
|
if (gc > 0) {
|
||||||
|
glyphsBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||||
|
ui.DispatchText(cmd, glyphsSlot, gc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.FinishInit();
|
||||||
|
window.Render();
|
||||||
|
window.StartUpdate();
|
||||||
|
window.StartSync();
|
||||||
|
}
|
||||||
25
examples/OptionsSpike/project.cpp
Normal file
25
examples/OptionsSpike/project.cpp
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import std;
|
||||||
|
import Crafter.Build;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||||
|
Configuration* graphics = LocalProject({
|
||||||
|
.projectFile = "../../project.cpp",
|
||||||
|
.args = std::vector<std::string>(args.begin(), args.end()),
|
||||||
|
});
|
||||||
|
|
||||||
|
Configuration cfg;
|
||||||
|
cfg.path = "./";
|
||||||
|
cfg.name = "OptionsSpike";
|
||||||
|
cfg.outputName = "OptionsSpike";
|
||||||
|
ApplyStandardArgs(cfg, args);
|
||||||
|
cfg.dependencies = { graphics };
|
||||||
|
|
||||||
|
std::array<fs::path, 0> ifaces = {};
|
||||||
|
std::array<fs::path, 1> impls = { "main" };
|
||||||
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
|
||||||
|
cfg.files.push_back("font.ttf");
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
180
implementations/Crafter.Graphics-InputField.cpp
Normal file
180
implementations/Crafter.Graphics-InputField.cpp
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
/*
|
||||||
|
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 version 3.0 as published by the Free Software Foundation;
|
||||||
|
|
||||||
|
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;
|
||||||
|
module Crafter.Graphics:InputField_impl;
|
||||||
|
import :InputField;
|
||||||
|
import :UI;
|
||||||
|
import :UIComponents;
|
||||||
|
import :Font;
|
||||||
|
import :Types;
|
||||||
|
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, CrafterKeys key) {
|
||||||
|
switch (key) {
|
||||||
|
case CrafterKeys::Backspace:
|
||||||
|
if (f.cursorPos > 0 && !f.value.empty()) {
|
||||||
|
f.value.erase(f.cursorPos - 1, 1);
|
||||||
|
--f.cursorPos;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CrafterKeys::Delete:
|
||||||
|
if (f.cursorPos < f.value.size()) {
|
||||||
|
f.value.erase(f.cursorPos, 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CrafterKeys::Left:
|
||||||
|
if (f.cursorPos > 0) --f.cursorPos;
|
||||||
|
break;
|
||||||
|
case CrafterKeys::Right:
|
||||||
|
if (f.cursorPos < f.value.size()) ++f.cursorPos;
|
||||||
|
break;
|
||||||
|
case CrafterKeys::Home:
|
||||||
|
f.cursorPos = 0;
|
||||||
|
break;
|
||||||
|
case CrafterKeys::End:
|
||||||
|
f.cursorPos = f.value.size();
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t Crafter::InputField_HitTestCursor(const InputField& f,
|
||||||
|
Rect rect,
|
||||||
|
float clickX,
|
||||||
|
Font& font, float fontSize,
|
||||||
|
const InputFieldColors& colors)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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()));
|
||||||
|
float caretX = textX + (sub.empty() ? 0.0f : static_cast<float>(font.GetLineWidth(sub, fontSize)));
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -184,3 +184,50 @@ void Crafter::DrawProgressBar(UIBuffer& buf, Rect r, float t01,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── DrawText ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
float Crafter::DrawText(UIBuffer& buf, std::string_view text,
|
||||||
|
float x, float baselineY,
|
||||||
|
Font& font, float fontSize,
|
||||||
|
std::array<float, 4> color,
|
||||||
|
TextAlign align)
|
||||||
|
{
|
||||||
|
if (text.empty() || buf.atlas == nullptr || buf.renderer == nullptr) return 0.0f;
|
||||||
|
if (buf.glyphs == nullptr || buf.glyphCount == nullptr) return 0.0f;
|
||||||
|
|
||||||
|
std::uint32_t before = *buf.glyphCount;
|
||||||
|
std::uint32_t cap = (buf.glyphCap > before) ? (buf.glyphCap - before) : 0;
|
||||||
|
if (cap == 0) return 0.0f;
|
||||||
|
|
||||||
|
GlyphItem* writePos = buf.glyphs + before;
|
||||||
|
float advance = 0.0f;
|
||||||
|
std::uint32_t n = buf.renderer->ShapeText(
|
||||||
|
font, fontSize, x, baselineY,
|
||||||
|
text, color, writePos, cap, &advance
|
||||||
|
);
|
||||||
|
*buf.glyphCount = before + n;
|
||||||
|
|
||||||
|
if (align != TextAlign::Left && n > 0) {
|
||||||
|
float shift = (align == TextAlign::Center) ? -advance * 0.5f : -advance;
|
||||||
|
for (std::uint32_t i = 0; i < n; ++i) writePos[i].x += shift;
|
||||||
|
}
|
||||||
|
return advance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DrawImage ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void Crafter::DrawImage(UIBuffer& buf, Rect r,
|
||||||
|
std::uint32_t textureSlot, std::uint32_t samplerSlot,
|
||||||
|
std::array<float, 4> tint,
|
||||||
|
std::array<float, 4> uv)
|
||||||
|
{
|
||||||
|
if (buf.images == nullptr || buf.imageCount == nullptr) return;
|
||||||
|
if (*buf.imageCount >= buf.imageCap) return;
|
||||||
|
buf.images[(*buf.imageCount)++] = ImageItem{
|
||||||
|
r.x, r.y, r.w, r.h,
|
||||||
|
uv[0], uv[1], uv[2], uv[3],
|
||||||
|
tint[0], tint[1], tint[2], tint[3],
|
||||||
|
textureSlot, samplerSlot, 0, 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
103
interfaces/Crafter.Graphics-InputField.cppm
Normal file
103
interfaces/Crafter.Graphics-InputField.cppm
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
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 version 3.0 as published by the Free Software Foundation;
|
||||||
|
|
||||||
|
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;
|
||||||
|
export module Crafter.Graphics:InputField;
|
||||||
|
import std;
|
||||||
|
import :Types;
|
||||||
|
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`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// else is ignored. Safe to feed every key the host receives.
|
||||||
|
void InputField_OnKey(InputField&, CrafterKeys);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
@ -38,8 +38,10 @@ import :FontAtlas;
|
||||||
// function body into your code and modify it. There is no override hook.
|
// function body into your code and modify it. There is no override hook.
|
||||||
|
|
||||||
export namespace Crafter {
|
export namespace Crafter {
|
||||||
// Aggregate for the two item buffers + the optional text-shaping deps.
|
// Aggregate for the standard item buffers + the optional text-shaping
|
||||||
// Build one per frame in onBuild and pass it to component calls.
|
// deps. Build one per frame in onBuild and pass it to component calls.
|
||||||
|
// Any pointer left null causes the corresponding component to be a no-op
|
||||||
|
// (so a frame that doesn't draw images can leave `images` unset).
|
||||||
struct UIBuffer {
|
struct UIBuffer {
|
||||||
QuadItem* quads = nullptr;
|
QuadItem* quads = nullptr;
|
||||||
std::uint32_t* quadCount = nullptr;
|
std::uint32_t* quadCount = nullptr;
|
||||||
|
|
@ -49,6 +51,10 @@ export namespace Crafter {
|
||||||
std::uint32_t* glyphCount = nullptr;
|
std::uint32_t* glyphCount = nullptr;
|
||||||
std::uint32_t glyphCap = 0;
|
std::uint32_t glyphCap = 0;
|
||||||
|
|
||||||
|
ImageItem* images = nullptr;
|
||||||
|
std::uint32_t* imageCount = nullptr;
|
||||||
|
std::uint32_t imageCap = 0;
|
||||||
|
|
||||||
FontAtlas* atlas = nullptr; // for text-emitting components
|
FontAtlas* atlas = nullptr; // for text-emitting components
|
||||||
UIRenderer* renderer = nullptr; // for ShapeText
|
UIRenderer* renderer = nullptr; // for ShapeText
|
||||||
};
|
};
|
||||||
|
|
@ -92,6 +98,9 @@ export namespace Crafter {
|
||||||
float cornerRadius = 0;
|
float cornerRadius = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Where the X coordinate sits relative to the emitted glyph run.
|
||||||
|
enum class TextAlign : std::uint8_t { Left, Center, Right };
|
||||||
|
|
||||||
// ─── component functions ───────────────────────────────────────────
|
// ─── component functions ───────────────────────────────────────────
|
||||||
|
|
||||||
// Background quad (color depends on state) + centered label glyphs.
|
// Background quad (color depends on state) + centered label glyphs.
|
||||||
|
|
@ -112,4 +121,22 @@ export namespace Crafter {
|
||||||
// Background quad + a filled quad clipped to t01 of the inner width.
|
// Background quad + a filled quad clipped to t01 of the inner width.
|
||||||
void DrawProgressBar(UIBuffer& buf, Rect r, float t01,
|
void DrawProgressBar(UIBuffer& buf, Rect r, float t01,
|
||||||
const ProgressColors& c);
|
const ProgressColors& c);
|
||||||
|
|
||||||
|
// Single-line text emit. `(x, baselineY)` is the start position; horizontal
|
||||||
|
// alignment shifts the run after shaping. Returns the advance width that
|
||||||
|
// was written. No line-wrap, no kerning — same shaping rules as
|
||||||
|
// UIRenderer::ShapeText (which this calls).
|
||||||
|
float DrawText(UIBuffer& buf, std::string_view text,
|
||||||
|
float x, float baselineY,
|
||||||
|
Font& font, float fontSize,
|
||||||
|
std::array<float, 4> color,
|
||||||
|
TextAlign align = TextAlign::Left);
|
||||||
|
|
||||||
|
// Sampled image quad. The texture and sampler must already have heap
|
||||||
|
// slots (UIRenderer::RegisterImage / RegisterSampler). `uv` is
|
||||||
|
// {u0, v0, u1, v1} into the source texture; defaults to the full image.
|
||||||
|
void DrawImage(UIBuffer& buf, Rect r,
|
||||||
|
std::uint32_t textureSlot, std::uint32_t samplerSlot,
|
||||||
|
std::array<float, 4> tint = {1, 1, 1, 1},
|
||||||
|
std::array<float, 4> uv = {0, 0, 1, 1});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,3 +42,4 @@ export import :FontAtlas;
|
||||||
export import :ComputeShader;
|
export import :ComputeShader;
|
||||||
export import :UI;
|
export import :UI;
|
||||||
export import :UIComponents;
|
export import :UIComponents;
|
||||||
|
export import :InputField;
|
||||||
|
|
@ -63,7 +63,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
|
|
||||||
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
|
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
|
||||||
|
|
||||||
std::array<fs::path, 23> ifaces = {
|
std::array<fs::path, 24> ifaces = {
|
||||||
"interfaces/Crafter.Graphics",
|
"interfaces/Crafter.Graphics",
|
||||||
"interfaces/Crafter.Graphics-Animation",
|
"interfaces/Crafter.Graphics-Animation",
|
||||||
"interfaces/Crafter.Graphics-ComputeShader",
|
"interfaces/Crafter.Graphics-ComputeShader",
|
||||||
|
|
@ -73,6 +73,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
"interfaces/Crafter.Graphics-FontAtlas",
|
"interfaces/Crafter.Graphics-FontAtlas",
|
||||||
"interfaces/Crafter.Graphics-ForwardDeclarations",
|
"interfaces/Crafter.Graphics-ForwardDeclarations",
|
||||||
"interfaces/Crafter.Graphics-ImageVulkan",
|
"interfaces/Crafter.Graphics-ImageVulkan",
|
||||||
|
"interfaces/Crafter.Graphics-InputField",
|
||||||
"interfaces/Crafter.Graphics-Mesh",
|
"interfaces/Crafter.Graphics-Mesh",
|
||||||
"interfaces/Crafter.Graphics-PipelineRTVulkan",
|
"interfaces/Crafter.Graphics-PipelineRTVulkan",
|
||||||
"interfaces/Crafter.Graphics-RenderingElement3D",
|
"interfaces/Crafter.Graphics-RenderingElement3D",
|
||||||
|
|
@ -88,11 +89,12 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
"interfaces/Crafter.Graphics-VulkanTransition",
|
"interfaces/Crafter.Graphics-VulkanTransition",
|
||||||
"interfaces/Crafter.Graphics-Window",
|
"interfaces/Crafter.Graphics-Window",
|
||||||
};
|
};
|
||||||
std::array<fs::path, 9> impls = {
|
std::array<fs::path, 10> impls = {
|
||||||
"implementations/Crafter.Graphics-ComputeShader",
|
"implementations/Crafter.Graphics-ComputeShader",
|
||||||
"implementations/Crafter.Graphics-Device",
|
"implementations/Crafter.Graphics-Device",
|
||||||
"implementations/Crafter.Graphics-Font",
|
"implementations/Crafter.Graphics-Font",
|
||||||
"implementations/Crafter.Graphics-FontAtlas",
|
"implementations/Crafter.Graphics-FontAtlas",
|
||||||
|
"implementations/Crafter.Graphics-InputField",
|
||||||
"implementations/Crafter.Graphics-Mesh",
|
"implementations/Crafter.Graphics-Mesh",
|
||||||
"implementations/Crafter.Graphics-RenderingElement3D",
|
"implementations/Crafter.Graphics-RenderingElement3D",
|
||||||
"implementations/Crafter.Graphics-UI",
|
"implementations/Crafter.Graphics-UI",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue