309 lines
12 KiB
C++
309 lines
12 KiB
C++
|
|
// 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();
|
||
|
|
}
|