// 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 kTabLabels = { "Graphics", "Input", "Audio" }; struct Layout { Rect canvas; Rect titleBar; Rect footer; Rect main; Rect tabStrip; Rect content; std::array 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 quadsBuf; VulkanBuffer 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 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 fields{ &widthField, &heightField }; std::array fieldRects{}; std::array 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 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 clickSub(&window.onMouseLeftClick, [&]() { for (int i = 0; i < 3; ++i) { if (tabHover[i]) { activeTab = static_cast(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 textSub(&window.onTextInput, [&](std::string_view t) { if (focusedField >= 0) InputField_OnText(*fields[focusedField], t); }); EventListener 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 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(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(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(); }