This commit is contained in:
Jorijn van der Graaf 2026-05-03 02:45:38 +02:00
commit c054f1e0b3
9 changed files with 699 additions and 5 deletions

Binary file not shown.

View 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();
}

View 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;
}

View 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,
};
}
}

View file

@ -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,
};
}

View 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);
}

View file

@ -38,8 +38,10 @@ import :FontAtlas;
// function body into your code and modify it. There is no override hook.
export namespace Crafter {
// Aggregate for the two item buffers + the optional text-shaping deps.
// Build one per frame in onBuild and pass it to component calls.
// Aggregate for the standard item buffers + the optional text-shaping
// 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 {
QuadItem* quads = nullptr;
std::uint32_t* quadCount = nullptr;
@ -49,6 +51,10 @@ export namespace Crafter {
std::uint32_t* glyphCount = nullptr;
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
UIRenderer* renderer = nullptr; // for ShapeText
};
@ -92,6 +98,9 @@ export namespace Crafter {
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 ───────────────────────────────────────────
// 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.
void DrawProgressBar(UIBuffer& buf, Rect r, float t01,
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});
}

View file

@ -41,4 +41,5 @@ export import :RTPass;
export import :FontAtlas;
export import :ComputeShader;
export import :UI;
export import :UIComponents;
export import :UIComponents;
export import :InputField;

View file

@ -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", ""});
std::array<fs::path, 23> ifaces = {
std::array<fs::path, 24> ifaces = {
"interfaces/Crafter.Graphics",
"interfaces/Crafter.Graphics-Animation",
"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-ForwardDeclarations",
"interfaces/Crafter.Graphics-ImageVulkan",
"interfaces/Crafter.Graphics-InputField",
"interfaces/Crafter.Graphics-Mesh",
"interfaces/Crafter.Graphics-PipelineRTVulkan",
"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-Window",
};
std::array<fs::path, 9> impls = {
std::array<fs::path, 10> impls = {
"implementations/Crafter.Graphics-ComputeShader",
"implementations/Crafter.Graphics-Device",
"implementations/Crafter.Graphics-Font",
"implementations/Crafter.Graphics-FontAtlas",
"implementations/Crafter.Graphics-InputField",
"implementations/Crafter.Graphics-Mesh",
"implementations/Crafter.Graphics-RenderingElement3D",
"implementations/Crafter.Graphics-UI",