User Interface
We follow the standard ribbon interface for UI. Further, supporting all indian scheduled languages is must. This will als enable us to support international languages in future. Please refer below actual code files for user interface design specifications. We have baseline strings say N numbers in English. Each string can have a corresponding language translation or if the language translation is empty string, than the corresponding english text shall be followed. All translations are stored in UserInterface-Text.h file.
Supporting all Indian languages is mostly a data organization + text shaping problem, not a rendering problem. Renderer will just draw glyphs; the system around it decides which string to show.
Translation Document
1// Copyright (c) 2026-Present : Ram Shanker: All rights reserved.
2#pragma once
3
4enum class UILanguage : uint8_t
5{
6 English = 0,
7
8 // 22 Indian scheduled languages
9 /* Here is the list of the given languages arranged in descending order of the number of speakers.
10 2011 Census of India data for total speakers, including both native/mother tongue and second-language speakers where reported,
11 as this is the most comprehensive official source available). */
12
13 Hindi, // ~528–691 million total speakers; ~43.63% of India's population as native speakers alone)
14 Bengali, // ~97–107 million
15 Marathi, // ~83–99 million
16 Telugu, // ~81–94 million
17 Tamil, // ~69–76 million
18 Gujarati, // ~55–60 million
19 Urdu, // ~50–63 million
20 Kannada, // ~43–58 million
21 Odia, // ~37–42 million
22 Malayalam, // ~34–35 million
23 Punjabi, // ~33–36 million
24 Assamese, // ~15–23 million
25 Maithili, // ~13–14 million, based on ~1.12% share)
26 Santali, // ~7.3–7.7 million
27 Kashmiri, // ~6.8–7 million
28 Nepali, // ~2.9–3 million
29 Sindhi, // ~2.7–3 million
30 Dogri, // ~2.6–2.8 million
31 Konkani, // ~2.2–2.6 million
32 Manipuri, // (Meitei) ~1.7–2 million
33 Bodo, // ~1.4–1.6 million
34 Sanskrit, // ~25,000 native speakers; higher if including those reporting knowledge, but still by far the smallest)
35
36 // Major global engineering languages. Population number by Grok citing
37 ChineseSimplified, // Both Chinese combined ~1.18–1.20 billion total speakers (mostly native)
38 ChineseTraditional, //(Mandarin Chinese)
39 Spanish, // ~558–560 million
40 Portuguese, // ~264–270 million
41 Russian, // ~253–260 million
42 French, // ~312–330 million (some sources place it slightly above or near Arabic depending on L2 counting)
43 Arabic, // ~335 million (Modern Standard Arabic + varieties; widely used in engineering contexts across the Middle East)
44 Indonesian, // ~200–255 million
45 German, // ~130–134 million
46 Japanese, // ~125–126 million. Covers all of Katakana , Kanji and Hiragana symbols within same fonts.
47 Vietnamese, // ~85–97 million
48 Turkish, // ~80–90 million
49 Persian, // (Farsi) — ~70–82 million. Farsi - Iran engineering market
50 Korean, // ~80–85 million
51 Italian, // ~65–90 million
52 Thai, // ~60–70 million
53 Polish, // ~45–50 million
54 Ukrainian, // ~35–45 million
55 Dutch, // ~25–30 million
56 Filipino, // (Tagalog) ~80–90 million total (native ~25–30 million + significant L2 in Philippines)
57 Swedish, // ~10–15 million
58 Czech, // ~10–12 million
59 Hungarian, // ~12–14 million
60
61 COUNT
62};
63
64/*
65ChatGPT analysis of population coverage by above 46 languages:
66
67| Metric | Result |
68| ------------------------------ | ---------- |
69| World population coverage | **90–94%** |
70| Engineering workforce coverage | **97–99%** |
71| India coverage | **~99%** |
72| Europe coverage | **~95%** |
73| Americas coverage | **~95%** |
74
75All these 46 languages translate to 13 unique scripts. Unicode handles all of them well.
76
77| Script | Languages |
78| ------------- | ------------------------------------- |
79| Latin | English, German, French, Spanish, etc |
80| Cyrillic | Russian, Ukrainian, Bulgarian, etc |
81| Devanagari | Hindi, Marathi, Nepali, Sanskrit etc |
82| Bengali | Bengali, Assamese |
83| Gurmukhi | Punjabi |
84| Gujarati | Gujarati |
85| Odia | Odia |
86| Tamil | Tamil |
87| Telugu | Telugu |
88| Kannada | Kannada |
89| Malayalam | Malayalam |
90| Meetei Mayek | Manipuri (Meitei) |
91| Ol Chiki | Santali |
92| Arabic script | Urdu, Arabic, Persian, Kashmiri |
93| Chinese Han | Chinese + Japanese Kanji |
94| Japanese kana | Hiragana/Katakana |
95| Hangul | Korean |
96| Thai | Thai |
97
98Professional CAD software language coverage (As per ChatGPT).
99| Software | Languages |
100| ---------- | --------- |
101| AutoCAD | ~15 |
102| SolidWorks | ~13 |
103| Fusion360 | ~10 |
104| CATIA | ~8 |
105All softwares listed below are copy right of respective software companies.
106
107HENCE OUR LANGUAGE LIST IS FROZEN ! ;)
108
109Estimated size overhead of bundling all the fonts:
110
111| Font | Typical Size |
112| ---------------------------- | ------------ |
113| Noto Sans (Latin + extended) | ~2 MB |
114| Noto Sans Devanagari | ~1.5 MB |
115| Noto Sans Bengali | ~1.3 MB |
116| Noto Sans Gurmukhi | ~0.9 MB |
117| Noto Sans Gujarati | ~1.0 MB |
118| Noto Sans Oriya (Odia) | ~1.1 MB |
119| Noto Sans Tamil | ~0.9 MB |
120| Noto Sans Telugu | ~1.2 MB |
121| Noto Sans Kannada | ~1.2 MB |
122| Noto Sans Malayalam | ~1.4 MB |
123| Noto Sans Arabic | ~1.2 MB |
124| Noto Sans Thai | ~0.7 MB |
125
126Subtotal (non-CJK): ≈ 14–15 MB
127
128| Font | Approx Size |
129| -------------------------------------- | ----------- |
130| Noto Sans CJK SC (Simplified Chinese) | ~16–18 MB |
131| Noto Sans CJK TC (Traditional Chinese) | ~16–18 MB |
132| Noto Sans CJK JP (Japanese) | ~16–18 MB |
133| Noto Sans CJK KR (Korean) | ~16–18 MB |
134
135CJK 3 variants (SC + JP + KR): ≈ 48–54 MB
136
137Total: ≈ 65 MB , ~60% Compression expected in Installer. ≈ 40 MB. Acceptable.
138
139Runtime: Entire font files will not be loaded at runtime.
140They will be loaded on demand to minimize memory footprint.
141
142*/User Interface Design Document and Implementation!
1// Copyright (c) 2026-Present : Ram Shanker: All rights reserved.
2#pragma once
3
4#define WIN32_LEAN_AND_MEAN
5#define NOMINMAX //
6#include <windows.h> // MUST be before d3d12.h
7#include <d3d12.h>
8#include <d3dx12.h>
9#include <dxgi1_6.h>
10#include <wrl.h>
11#include <vector>
12#include <array>
13#include <unordered_map>
14#include <iostream>
15#include <atomic>
16
17#include "ConstantsApplication.h"
18#include "UserInterface.h" // It also includes "UserInterface-TextTranslations.h"
19
20// Do not #include "विश्वकर्मा.h" otherwise it will lead to circular dependency error. Declare this struct exist.
21struct SingleUIWindow; // Add this forward declaration:
22
23using Microsoft::WRL::ComPtr;
24
25struct DX12ResourcesUI { // GPU resources
26 std::array<ComPtr<ID3D12Resource>, UI_MAX_ATLAS_TEXTURES> uiAtlasTextures; // 1024×1024 or 2048×2048 RGBA (or R8 for alpha-only)
27 ComPtr<ID3D12Resource> uiVertexBuffer; // Dynamic upload buffer for vertices
28 ComPtr<ID3D12Resource> uiIndexBuffer; // Dynamic upload buffer for indices
29
30 UINT8* pVertexDataBegin = nullptr; // Mapped pointer for immediate writing
31 UINT8* pIndexDataBegin = nullptr;
32 UINT8* pOrthoDataBegin = nullptr;
33
34 ComPtr<ID3D12PipelineState> uiPSO;
35 ComPtr<ID3D12RootSignature> uiRootSignature;
36 ComPtr<ID3D12Resource> uiOrthoConstantBuffer;
37 ComPtr<ID3D12DescriptorHeap> srvHeap;
38 ComPtr<ID3D12DescriptorHeap> samplerHeap;
39
40 uint32_t maxVertices = 65536;
41 uint32_t maxIndices = 65536 * 3;
42};
43
44struct UIDrawContext { // Draw context
45 UIVertex* vertexPtr;
46 uint16_t* indexPtr;
47 uint32_t vertexCount, indexCount;
48};
49
50// DirectX12 Immediate Mode UI System (Phase 4A). Tab Bar Rendering Only
51// External interfaces of User Interface sub module of the code.
52void InitUIResources(DX12ResourcesUI& uiRes, ID3D12Device* device);
53void CleanupUIResources(DX12ResourcesUI& uiRes);
54
55void PushRect(UIDrawContext& ctx, float x, float y, float w, float h, uint32_t color, DX12ResourcesUI& uiRes);
56void PushRoundedRectangle(UIDrawContext& ctx, float x, float y, float w, float h, float radiusPx,
57 uint32_t color, DX12ResourcesUI& uiRes);
58void PushTopRoundedRectangle(UIDrawContext& ctx, float x, float y, float w, float h, float radiusPx,
59 uint32_t color, DX12ResourcesUI& uiRes);
60void PushText(UIDrawContext& ctx, float x, float y, const char* text, uint32_t color, DX12ResourcesUI& uiRes);
61
62// Slots 0 and 1 are currently reserved for the mandatory English and Icon atlases.
63// Future script atlases can use slots [UI_FIRST_DYNAMIC_SCRIPT_ATLAS_SLOT, UI_MAX_ATLAS_TEXTURES).
64bool UploadUIAtlasTexture(DX12ResourcesUI& uiRes, ID3D12Device* device, uint32_t atlasSlot,
65 const AtlasBitmap& atlas);
66
67void PrecomputeTopRibbonLayout(UITopRibbonLayout& layout, float monitorDPIX, float monitorDPIY);
68
69void RenderUIOverlay(SingleUIWindow& window, ID3D12GraphicsCommandList* cmdList,
70 DX12ResourcesUI& uiRes, UITopRibbonLayout& topRibbonLayout,
71 float monitorDPIX, float monitorDPIY, const UIInput& input);
1// Copyright (c) 2026-Present : Ram Shanker: All rights reserved.
2
3#include "UserInterface-DirectX12.h"
4#include <algorithm>
5#include <d3dcompiler.h>
6#include "ShaderUIVertex.h"
7#include "ShaderUIPixel.h"
8#include "FontManager.h"
9#include "..\build\NotoSansMSDF_Compiled.h"
10#include <MemoryManagerGPU-DirectX12.h>
11#include "विश्वकर्मा.h"
12#include "TextureSaver.h"
13#include "UserInterfaceTranslationCompiled.h"
14#include <array>
15#include <cmath>
16extern शंकर gpu;
17extern std::atomic<uint16_t*> publishedTabIndexes;
18extern std::atomic<uint16_t> publishedTabCount;
19extern void PrintHResult(int);
20std::atomic<uint32_t> actionWriteIndex;
21// ASCII Character set.
22std::string charset = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
23std::atomic<uint64_t> atlasFence = 0;
24
25struct UIAtlasRegion {
26 float uvMinX = 0.0f;
27 float uvMinY = 0.0f;
28 float uvMaxX = 0.0f;
29 float uvMaxY = 0.0f;
30};
31
32struct UIRoundedRectangleNineSlice {
33 std::array<std::array<UIAtlasRegion, 3>, 3> regions{};
34};
35
36struct UIIconAtlasMetadata {
37 UIRoundedRectangleNineSlice roundedRectangle{};
38 std::array<char32_t, 4> dummyIconCodepoints{ U'\uE100', U'\uE101', U'\uE102', U'\uE103' };
39 std::vector<char32_t> mixedIconCodepoints{};
40};
41
42static UIIconAtlasMetadata gIconAtlasMetadata{};
43
44static UIAtlasRegion MakeAtlasRegion(int x, int y, int w, int h, int atlasW, int atlasH) {
45 UIAtlasRegion region{};
46 region.uvMinX = (float)x / (float)atlasW;
47 region.uvMinY = (float)y / (float)atlasH;
48 region.uvMaxX = (float)(x + w) / (float)atlasW;
49 region.uvMaxY = (float)(y + h) / (float)atlasH;
50 return region;
51}
52
53UIColors uiLightDefaultColors, uiActiveColors; // Initialized to default light theme colors.
54
55static void GenerateRoundedRectangleNineSlice(AtlasBitmap& atlas, int originX, int originY,
56 int sourceSizePx, int sourceRadiusPx, UIRoundedRectangleNineSlice& outSlice) {
57 const int atlasW = atlas.width;
58 const int atlasH = atlas.height;
59
60 for (int y = 0; y < sourceSizePx; ++y) {
61 for (int x = 0; x < sourceSizePx; ++x) {
62 const float px = (float)x + 0.5f;
63 const float py = (float)y + 0.5f;
64 const float nearestX = std::clamp(px, (float)sourceRadiusPx,
65 (float)(sourceSizePx - sourceRadiusPx));
66 const float nearestY = std::clamp(py, (float)sourceRadiusPx,
67 (float)(sourceSizePx - sourceRadiusPx));
68 const float dx = px - nearestX;
69 const float dy = py - nearestY;
70 const float distance = std::sqrt(dx * dx + dy * dy);
71 const float coverage = std::clamp((float)sourceRadiusPx + 0.5f - distance, 0.0f, 1.0f);
72
73 atlas.pixels[(originY + y) * atlasW + (originX + x)] = (uint8_t)std::round(coverage * 255.0f);
74 }
75 }
76
77 const int middle = sourceSizePx - 2 * sourceRadiusPx;
78 const int widths[3] = { sourceRadiusPx, middle, sourceRadiusPx };
79 const int heights[3] = { sourceRadiusPx, middle, sourceRadiusPx };
80 int yCursor = originY;
81 for (int row = 0; row < 3; ++row) {
82 int xCursor = originX;
83 for (int col = 0; col < 3; ++col) {
84 outSlice.regions[row][col] =
85 MakeAtlasRegion(xCursor, yCursor, widths[col], heights[row], atlasW, atlasH);
86 xCursor += widths[col];
87 }
88 yCursor += heights[row];
89 }
90}
91
92static void FillRect(AtlasBitmap& atlas, int x, int y, int w, int h, uint8_t coverage) {
93 for (int yy = y; yy < y + h; ++yy) {
94 for (int xx = x; xx < x + w; ++xx) {
95 atlas.pixels[yy * atlas.width + xx] = coverage;
96 }
97 }
98}
99
100static bool IsPrivateUseCodepoint(char32_t codepoint) {
101 return (codepoint >= 0xE000 && codepoint <= 0xF8FF) ||
102 (codepoint >= 0xF0000 && codepoint <= 0xFFFFD) ||
103 (codepoint >= 0x100000 && codepoint <= 0x10FFFD);
104}
105
106static bool TryReserveIconCell(int iconIndex, int atlasW, int atlasH, int& outX, int& outY) {
107 constexpr int iconCellSize = 24;
108 constexpr int iconCellGap = 4;
109 constexpr int iconStartY = 48;
110 const int cellsPerRow = (atlasW + iconCellGap) / (iconCellSize + iconCellGap);
111 if (cellsPerRow <= 0) return false;
112
113 outX = (iconIndex % cellsPerRow) * (iconCellSize + iconCellGap);
114 outY = iconStartY + (iconIndex / cellsPerRow) * (iconCellSize + iconCellGap);
115 return outX + iconCellSize <= atlasW && outY + iconCellSize <= atlasH;
116}
117
118static void StoreIconCellGlyph(char32_t codepoint, int x, int y, int atlasW, int atlasH) {
119 constexpr int iconCellSize = 24;
120 Glyph glyph{};
121 glyph.uvMinX = (float)x / atlasW;
122 glyph.uvMinY = (float)y / atlasH;
123 glyph.uvMaxX = (float)(x + iconCellSize) / atlasW;
124 glyph.uvMaxY = (float)(y + iconCellSize) / atlasH;
125 glyph.width = iconCellSize;
126 glyph.height = iconCellSize;
127 glyph.advanceX = iconCellSize;
128 iconGlyphLookup[codepoint] = glyph;
129 gIconAtlasMetadata.mixedIconCodepoints.push_back(codepoint);
130}
131
132static AtlasBitmap BuildIconAtlas() {
133 constexpr int atlasW = 256;
134 constexpr int atlasH = 256;
135 constexpr int iconCellSize = 24;
136 constexpr int proceduralIconDrawSize = 20;
137 AtlasBitmap atlas{};
138 atlas.width = atlasW;
139 atlas.height = atlasH;
140 atlas.pixels.resize(atlasW * atlasH, 0);
141
142 // The source rounded rectangle is split into 9 texture regions at draw time.
143 // Destination corners are resized to ~2 mm in screen space by PushRoundedRectangle.
144 GenerateRoundedRectangleNineSlice(atlas, 0, 0, 32, 8, gIconAtlasMetadata.roundedRectangle);
145
146 iconGlyphLookup.clear();
147 gIconAtlasMetadata.mixedIconCodepoints.clear();
148
149 std::array<int, 4> iconXs{};
150 std::array<int, 4> iconYs{};
151 for (int i = 0; i < 4; ++i) {
152 if (!TryReserveIconCell(i, atlasW, atlasH, iconXs[i], iconYs[i])) continue;
153 StoreIconCellGlyph(gIconAtlasMetadata.dummyIconCodepoints[i], iconXs[i], iconYs[i], atlasW, atlasH);
154 }
155
156 // Dummy icon 0: plus
157 FillRect(atlas, iconXs[0] + 10, iconYs[0] + 4, 4, 16, 255);
158 FillRect(atlas, iconXs[0] + 4, iconYs[0] + 10, 16, 4, 255);
159
160 // Dummy icon 1: folder-like block
161 FillRect(atlas, iconXs[1] + 4, iconYs[1] + 8, 16, 11, 255);
162 FillRect(atlas, iconXs[1] + 6, iconYs[1] + 5, 7, 4, 255);
163
164 // Dummy icon 2: ring
165 for (int y = 0; y < proceduralIconDrawSize; ++y) {
166 for (int x = 0; x < proceduralIconDrawSize; ++x) {
167 const float dx = (float)x + 0.5f - 10.0f;
168 const float dy = (float)y + 0.5f - 10.0f;
169 const float d = std::sqrt(dx * dx + dy * dy);
170 if (d >= 5.0f && d <= 8.0f) {
171 atlas.pixels[(iconYs[2] + 2 + y) * atlasW + (iconXs[2] + 2 + x)] = 255;
172 }
173 }
174 }
175
176 // Dummy icon 3: 2x2 grid
177 FillRect(atlas, iconXs[3] + 4, iconYs[3] + 4, 7, 7, 255);
178 FillRect(atlas, iconXs[3] + 13, iconYs[3] + 4, 7, 7, 255);
179 FillRect(atlas, iconXs[3] + 4, iconYs[3] + 13, 7, 7, 255);
180 FillRect(atlas, iconXs[3] + 13, iconYs[3] + 13, 7, 7, 255);
181
182 if (ftIconFace) {
183 FT_Set_Pixel_Sizes(ftIconFace, 0, proceduralIconDrawSize);
184
185 FT_UInt glyphIndex = 0;
186 FT_ULong charCode = FT_Get_First_Char(ftIconFace, &glyphIndex);
187 int iconIndex = (int)gIconAtlasMetadata.mixedIconCodepoints.size();
188 while (glyphIndex != 0) {
189 const char32_t codepoint = (char32_t)charCode;
190 if (IsPrivateUseCodepoint(codepoint) &&
191 std::find(gIconAtlasMetadata.dummyIconCodepoints.begin(),
192 gIconAtlasMetadata.dummyIconCodepoints.end(), codepoint) ==
193 gIconAtlasMetadata.dummyIconCodepoints.end() &&
194 FT_Load_Char(ftIconFace, charCode, FT_LOAD_RENDER) == 0) {
195 int cellX = 0;
196 int cellY = 0;
197 if (!TryReserveIconCell(iconIndex, atlasW, atlasH, cellX, cellY)) break;
198
199 FT_GlyphSlot g = ftIconFace->glyph;
200 const int bitmapX = cellX + std::max(0, (iconCellSize - (int)g->bitmap.width) / 2);
201 const int bitmapY = cellY + std::max(0, (iconCellSize - (int)g->bitmap.rows) / 2);
202 const int copyW = std::min((int)g->bitmap.width, iconCellSize);
203 const int copyH = std::min((int)g->bitmap.rows, iconCellSize);
204 for (int y = 0; y < copyH; ++y) {
205 for (int x = 0; x < copyW; ++x) {
206 atlas.pixels[(bitmapY + y) * atlasW + (bitmapX + x)] =
207 g->bitmap.buffer[y * g->bitmap.pitch + x];
208 }
209 }
210
211 StoreIconCellGlyph(codepoint, cellX, cellY, atlasW, atlasH);
212 ++iconIndex;
213 }
214
215 charCode = FT_Get_Next_Char(ftIconFace, charCode, &glyphIndex);
216 }
217 }
218
219 return atlas;
220}
221
222static AtlasBitmap BuildMSDFFontAtlas() {
223 AtlasBitmap atlas{};
224 atlas.width = NotoSansMSDF_Width;
225 atlas.height = NotoSansMSDF_Height;
226 atlas.bytesPerPixel = 4;
227 atlas.pixels.assign(NotoSansMSDF_Pixels,
228 NotoSansMSDF_Pixels + (size_t)atlas.width * (size_t)atlas.height * (size_t)atlas.bytesPerPixel);
229
230 glyphLookup.clear();
231 for (const auto& entry : NotoSansMSDF_Glyphs) {
232 const char32_t codepoint = entry.first;
233 const MSDFGlyph& msdf = entry.second;
234
235 Glyph glyph{};
236 glyph.uvMinX = msdf.atlasLeft / (float)atlas.width;
237 glyph.uvMaxX = msdf.atlasRight / (float)atlas.width;
238 glyph.uvMinY = ((float)atlas.height - msdf.atlasTop) / (float)atlas.height;
239 glyph.uvMaxY = ((float)atlas.height - msdf.atlasBottom) / (float)atlas.height;
240
241 glyph.width = std::max(0, (int)std::ceil((msdf.planeRight - msdf.planeLeft) * NotoSansMSDF_Size));
242 glyph.height = std::max(0, (int)std::ceil((msdf.planeTop - msdf.planeBottom) * NotoSansMSDF_Size));
243 glyph.bearingX = (int)std::floor(msdf.planeLeft * NotoSansMSDF_Size);
244 glyph.bearingY = (int)std::ceil(msdf.planeTop * NotoSansMSDF_Size);
245 glyph.advanceX = std::max(0, (int)std::round(msdf.advance * NotoSansMSDF_Size));
246
247 glyphLookup[codepoint] = glyph;
248 }
249
250 return atlas;
251}
252
253static uint32_t StableRandomUIColour(uint32_t seed) {
254 seed ^= seed >> 16;
255 seed *= 0x7FEB352Du;
256 seed ^= seed >> 15;
257 seed *= 0x846CA68Bu;
258 seed ^= seed >> 16;
259
260 uint32_t r = 80u + ((seed >> 0) & 0x7Fu);
261 uint32_t g = 80u + ((seed >> 8) & 0x7Fu);
262 uint32_t b = 80u + ((seed >> 16) & 0x7Fu);
263 return 0xFF000000u | (b << 16) | (g << 8) | r;
264}
265
266bool SubmitTextureUpload(const TextureUploadDesc& desc,
267 ComPtr<ID3D12Resource>* outTex, std::atomic<uint64_t>* fenceOut) {
268
269 uint32_t index = gUploadQueue.writeIndex.fetch_add(1, std::memory_order_relaxed);
270 UploadRequest& req = gUploadQueue.requests[index % MAX_UPLOAD_REQUESTS];
271
272 req.type = UploadType::Texture2D;
273 req.texture = desc;
274 req.outResource = outTex;
275 req.completionFence = fenceOut;
276
277 return true;
278}
279
280bool UploadUIAtlasTexture(DX12ResourcesUI& uiRes, ID3D12Device* device, uint32_t atlasSlot,
281 const AtlasBitmap& atlas) {
282 if (!device || atlasSlot >= UI_MAX_ATLAS_TEXTURES || atlas.width <= 0 || atlas.height <= 0 ||
283 atlas.pixels.empty() || (atlas.bytesPerPixel != 1 && atlas.bytesPerPixel != 4)) {
284 return false;
285 }
286
287 TextureUploadDesc desc = {};
288 desc.width = atlas.width;
289 desc.height = atlas.height;
290 desc.format = atlas.bytesPerPixel == 4 ? DXGI_FORMAT_R8G8B8A8_UNORM : DXGI_FORMAT_R8_UNORM;
291 desc.pixels = atlas.pixels.data();
292 desc.rowPitch = atlas.width * atlas.bytesPerPixel;
293
294 std::atomic<uint64_t> uploadFence = 0;
295 SubmitTextureUpload(desc, &uiRes.uiAtlasTextures[atlasSlot], &uploadFence);
296
297 uint64_t atlasReadyFence = gpu.copyFenceValue.fetch_add(1, std::memory_order_relaxed);
298 uploadFence.store(atlasReadyFence, std::memory_order_release);
299 toCopyThreadCV.notify_one();
300
301 if (gpu.copyFence->GetCompletedValue() < atlasReadyFence) {
302 ThrowIfFailed(gpu.copyFence->SetEventOnCompletion(atlasReadyFence, gpu.copyFenceEvent));
303 WaitForSingleObject(gpu.copyFenceEvent, INFINITE);
304 }
305
306 D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
307 srvDesc.Format = desc.format;
308 srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
309 srvDesc.Texture2D.MipLevels = 1;
310 srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
311
312 CD3DX12_CPU_DESCRIPTOR_HANDLE srvHandle(uiRes.srvHeap->GetCPUDescriptorHandleForHeapStart(),
313 atlasSlot, device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV));
314 device->CreateShaderResourceView(uiRes.uiAtlasTextures[atlasSlot].Get(), &srvDesc, srvHandle);
315 return true;
316}
317
318void InitUIResources( DX12ResourcesUI& uiRes, ID3D12Device* device) {
319 if (!InitFontSystem()) { // FONT system initialization (CPU-side)
320 std::cerr << "Font system initialization failed failed\n";
321 return;
322 }
323
324 // Root signature
325 CD3DX12_DESCRIPTOR_RANGE1 ranges[2]; // Descriptor ranges
326
327 // Range 0: SRV (t0) → from srvHeap // 1: 1 Texture, 0: register t0 = atlas
328 ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, UI_MAX_ATLAS_TEXTURES, 0, 0,
329 D3D12_DESCRIPTOR_RANGE_FLAG_NONE);
330 // Range 1: SAMPLER (s0) → from samplerHeap // 1: 1 Sampler, 0: register s0 = sampler
331 ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_NONE);
332
333 CD3DX12_ROOT_PARAMETER1 rootParams[3];
334 rootParams[0].InitAsConstantBufferView(0, 0, D3D12_ROOT_DESCRIPTOR_FLAG_NONE,
335 D3D12_SHADER_VISIBILITY_VERTEX);// b0 - Ortho constant buffer (vertex shader)
336
337 // Root Parameter 1: Descriptor Table containing only the SRV
338 rootParams[1].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_PIXEL);
339
340 // Root Parameter 2: Descriptor Table containing only the SAMPLER
341 rootParams[2].InitAsDescriptorTable(1, &ranges[1],
342 D3D12_SHADER_VISIBILITY_PIXEL);
343
344 CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootDesc;
345 rootDesc.Init_1_1(_countof(rootParams), rootParams, 0, nullptr,
346 D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
347
348 ComPtr<ID3DBlob> signature;
349 ComPtr<ID3DBlob> errorBlob;
350
351 HRESULT hr = D3DX12SerializeVersionedRootSignature(&rootDesc,
352 D3D_ROOT_SIGNATURE_VERSION_1_1, &signature, &errorBlob);
353 if (FAILED(hr)) {
354 if (errorBlob)
355 std::cerr << "Root Signature Serialization Failed:\n"
356 << (char*)errorBlob->GetBufferPointer() << std::endl;
357 ThrowIfFailed(hr); // will print the real error
358 }
359
360 ThrowIfFailed(device->CreateRootSignature(0, signature->GetBufferPointer(),
361 signature->GetBufferSize(), IID_PPV_ARGS(&uiRes.uiRootSignature)));
362
363 // Create SRV descriptor heap (1 texture)
364 D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
365 heapDesc.NumDescriptors = UI_MAX_ATLAS_TEXTURES;
366 heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
367 heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
368 ThrowIfFailed(device->CreateDescriptorHeap( &heapDesc, IID_PPV_ARGS(&uiRes.srvHeap) ));
369
370 // Create SAMPLER descriptor heap (shader-visible)
371 D3D12_DESCRIPTOR_HEAP_DESC samplerHeapDesc = {};
372 samplerHeapDesc.NumDescriptors = 1;
373 samplerHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
374 samplerHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
375 ThrowIfFailed(device->CreateDescriptorHeap(&samplerHeapDesc,
376 IID_PPV_ARGS(&uiRes.samplerHeap)));
377
378 // Create the actual sampler.
379 D3D12_SAMPLER_DESC samplerDesc = {};
380 samplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
381 samplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
382 samplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
383 samplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
384 samplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;
385 samplerDesc.MinLOD = 0;
386 samplerDesc.MaxLOD = D3D12_FLOAT32_MAX;
387
388 device->CreateSampler(&samplerDesc,
389 uiRes.samplerHeap->GetCPUDescriptorHandleForHeapStart());
390
391 // Shaders are compiled to DXIL during the build and embedded into the executable.
392 // Input layout
393
394 D3D12_INPUT_ELEMENT_DESC layout[] = {
395 { "POSITION",0,DXGI_FORMAT_R32G32_FLOAT,0,0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0 },
396 { "TEXCOORD",0,DXGI_FORMAT_R32G32_FLOAT,0,8, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0 },
397 { "COLOR",0,DXGI_FORMAT_R32_UINT,0,16, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0 },
398 { "TEXCOORD",1,DXGI_FORMAT_R32_UINT,0,20, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0 }
399 };
400
401 // PSO
402 D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
403 psoDesc.InputLayout = { layout,_countof(layout) };
404 psoDesc.pRootSignature = uiRes.uiRootSignature.Get();
405 psoDesc.VS = CD3DX12_SHADER_BYTECODE(g_uiVertexShader, sizeof(g_uiVertexShader));
406 psoDesc.PS = CD3DX12_SHADER_BYTECODE(g_uiPixelShader, sizeof(g_uiPixelShader));
407 psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
408 psoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
409 psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
410 psoDesc.BlendState.RenderTarget[0].BlendEnable = TRUE;
411 psoDesc.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_SRC_ALPHA;
412 psoDesc.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
413 psoDesc.DepthStencilState.DepthEnable = FALSE;
414 psoDesc.SampleMask = UINT_MAX;
415 psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
416 psoDesc.NumRenderTargets = 1;
417 psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
418 psoDesc.SampleDesc.Count = 1;
419
420 ThrowIfFailed( device->CreateGraphicsPipelineState( &psoDesc, IID_PPV_ARGS(&uiRes.uiPSO)));
421
422 // Vertex buffer
423 auto uploadHeap = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
424
425 auto vbDesc = CD3DX12_RESOURCE_DESC::Buffer( uiRes.maxVertices * sizeof(UIVertex));
426 ThrowIfFailed( device->CreateCommittedResource( &uploadHeap, D3D12_HEAP_FLAG_NONE, &vbDesc,
427 D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&uiRes.uiVertexBuffer)));
428
429 auto ibDesc = CD3DX12_RESOURCE_DESC::Buffer( uiRes.maxIndices * sizeof(uint16_t));
430 ThrowIfFailed( device->CreateCommittedResource( &uploadHeap, D3D12_HEAP_FLAG_NONE, &ibDesc,
431 D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&uiRes.uiIndexBuffer)));
432
433 auto cbDesc = CD3DX12_RESOURCE_DESC::Buffer(256);
434 ThrowIfFailed( device->CreateCommittedResource( &uploadHeap, D3D12_HEAP_FLAG_NONE, &cbDesc,
435 D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&uiRes.uiOrthoConstantBuffer)));
436
437 CD3DX12_RANGE readRange(0, 0);
438 uiRes.uiVertexBuffer->Map( 0, &readRange, reinterpret_cast<void**>(&uiRes.pVertexDataBegin));
439 uiRes.uiIndexBuffer->Map( 0, &readRange, reinterpret_cast<void**>(&uiRes.pIndexDataBegin));
440 uiRes.uiOrthoConstantBuffer->Map( 0, &readRange, reinterpret_cast<void**>(&uiRes.pOrthoDataBegin));
441 std::wcout << L"UI Resources Initialized (Phase 4A)\n";
442
443 AtlasBitmap englishAtlas = BuildMSDFFontAtlas();
444 AtlasBitmap iconAtlas = BuildIconAtlas();
445
446 TextureUploadDesc desc = {};
447 desc.width = englishAtlas.width;
448 desc.height = englishAtlas.height;
449 desc.format = DXGI_FORMAT_R8G8B8A8_UNORM;
450 desc.pixels = englishAtlas.pixels.data();
451 desc.rowPitch = englishAtlas.width * englishAtlas.bytesPerPixel;
452
453 int bytesPerPixel = 1;
454
455 SubmitTextureUpload(desc, &uiRes.uiAtlasTextures[UI_ENGLISH_ATLAS_SLOT], &atlasFence);// Enqueue the upload through upload queue
456 // RESERVED FENCE VALUE FOR THIS UPLOAD (this is the key change)
457 // The copy thread MUST eventually signal exactly this value.
458 uint64_t atlasReadyFence = gpu.copyFenceValue.fetch_add(1, std::memory_order_relaxed);
459 // Tell everyone (including the render thread) what fence value to wait for
460 atlasFence.store(atlasReadyFence, std::memory_order_release);
461 toCopyThreadCV.notify_one(); // Wakeup CPU thread to process the newly uploaded texture.
462 // CPU-blocking wait until Copy Queue has processed this upload
463 if (gpu.copyFence->GetCompletedValue() < atlasReadyFence) {
464 ThrowIfFailed(gpu.copyFence->SetEventOnCompletion(atlasReadyFence, gpu.copyFenceEvent));
465 WaitForSingleObject(gpu.copyFenceEvent, INFINITE); // CPU blocks here
466 }
467
468 // Now the texture is in DEFAULT heap → safe to create SRV
469 D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
470 srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
471 srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
472 srvDesc.Texture2D.MipLevels = 1;
473 srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
474
475 device->CreateShaderResourceView(uiRes.uiAtlasTextures[UI_ENGLISH_ATLAS_SLOT].Get(), &srvDesc,
476 uiRes.srvHeap->GetCPUDescriptorHandleForHeapStart());
477
478 SaveToBmp("icon_atlas_debug.bmp", iconAtlas.pixels.data(),
479 iconAtlas.width, iconAtlas.height, bytesPerPixel);
480 UploadUIAtlasTexture(uiRes, device, UI_ICON_ATLAS_SLOT, iconAtlas);
481
482 std::wcout << L"Mandatory UI atlases uploaded: English slot " << UI_ENGLISH_ATLAS_SLOT
483 << L", icon slot " << UI_ICON_ATLAS_SLOT << L"\n";
484}
485
486// Cleanup
487void CleanupUIResources(DX12ResourcesUI& uiRes) {
488 if (uiRes.uiVertexBuffer) uiRes.uiVertexBuffer->Unmap(0, nullptr);
489 if (uiRes.uiIndexBuffer) uiRes.uiIndexBuffer->Unmap(0, nullptr);
490 if (uiRes.uiOrthoConstantBuffer) uiRes.uiOrthoConstantBuffer->Unmap(0, nullptr);
491
492 uiRes = {};
493}
494
495static float TextScaleForHeight(float targetHeight) {
496 auto glyphIt = glyphLookup.find(U'M');
497 if (glyphIt == glyphLookup.end() || glyphIt->second.height <= 0) return 1.0f;
498
499 return targetHeight / (float)glyphIt->second.height;
500}
501
502static float MeasureUIStringWidth(const char32_t* text, float scale) {
503 if (!text) return 0.0f;
504
505 float cursorX = 0.0f;
506 float maxRight = 0.0f;
507
508 for (const char32_t* p = text; *p; ++p) {
509 auto glyphIt = glyphLookup.find(*p);
510 if (glyphIt == glyphLookup.end()) continue;
511
512 const Glyph& g = glyphIt->second;
513 float glyphLeft = cursorX + (float)g.bearingX * scale;
514 float glyphRight = glyphLeft + (float)g.width * scale;
515 if (glyphRight > maxRight) maxRight = glyphRight;
516 cursorX += (float)g.advanceX * scale;
517 }
518
519 return std::max(cursorX, maxRight);
520}
521
522static const char32_t* LocalizedUIString(UITextID stringID) {
523 const char32_t* text = GetUILocalizedString(stringID, UILanguage::English);
524 return text ? text : U"";
525}
526
527static const char32_t* LocalizedControlLabel(const UIControlDefinition& ctrl) {
528 const char32_t* label = LocalizedUIString(ctrl.nameStringID);
529 if (*label != U'\0') return label;
530
531 if (ctrl.action == Commands::INVALID || ctrl.type == 0 || ctrl.type == 3) {
532 return LocalizedUIString(ctrl.nameStringID);
533 }
534
535 return U"";
536}
537
538static void ClampTopRibbonScroll(UITopRibbonLayout& layout, float viewportWidth) {
539 const float maxScroll = std::max(0.0f, layout.totalContentWidthPx - viewportWidth);
540 layout.scrollOffsetPx = std::clamp(layout.scrollOffsetPx, 0.0f, maxScroll);
541}
542
543void PrecomputeTopRibbonLayout(UITopRibbonLayout& layout, float monitorDPIX, float monitorDPIY) {
544 const float previousScroll = layout.scrollOffsetPx;
545 layout = UITopRibbonLayout{};
546
547 layout.dpiX = monitorDPIX;
548 layout.dpiY = monitorDPIY;
549
550 const float pixelsPerMMx = monitorDPIX / 25.4f;
551 const float pixelsPerMMy = monitorDPIY / 25.4f;
552 layout.buttonWidthPx = std::round(UI_BUTTON_WIDTH_MM * pixelsPerMMx);
553 layout.iconSizePx = std::round(UI_ICON_SIZE_MM * pixelsPerMMy);
554 layout.textHeightPx = std::round(UI_TEXT_HEIGHT_MM * pixelsPerMMy);
555 layout.buttonHeightPx = std::max(std::round(UI_BUTTON_HEIGHT_MM * pixelsPerMMy),
556 std::max(layout.iconSizePx, layout.textHeightPx) + 4.0f);
557 layout.iconReservedWidthPx = layout.iconSizePx + 4.0f;
558 layout.textStartOffsetPx = layout.iconReservedWidthPx + 4.0f;
559 layout.textEndInsetPx = 6.0f;
560 layout.buttonGapPx = UI_BUTTON_GAP_MM * pixelsPerMMx;
561 layout.tabBarHeightPx = std::round(UI_TAB_BAR_HEIGHT_MM * pixelsPerMMy);
562 layout.roundedCornerRadiusPx = std::max(1.0f, std::round(UI_BUTTON_CORNER_RADIUS_MM * pixelsPerMMy));
563 layout.uiTextScale = TextScaleForHeight(layout.textHeightPx);
564 layout.actionGroupLabelY = (UI_TAB_BAR_HEIGHT_MM + UI_DIVIDER_GAP_PX) * pixelsPerMMy;
565 layout.actionGroupLabelHeightPx = UI_ACTION_GROUP_LABEL_HEIGHT_MM * pixelsPerMMy;
566 layout.topActionGroupY = (UI_TAB_BAR_HEIGHT_MM + UI_DIVIDER_GAP_PX +
567 UI_ACTION_GROUP_LABEL_HEIGHT_MM) * pixelsPerMMy + 5.0f;
568 layout.actionSubGroupLabelY = (UI_TAB_BAR_HEIGHT_MM + UI_DIVIDER_GAP_PX +
569 UI_ACTION_GROUP_LABEL_HEIGHT_MM + UI_DIVIDER_GAP_PX +
570 UI_ACTION_GROUP_HEIGHT_MM + UI_DIVIDER_GAP_PX) * pixelsPerMMy + 5.0f;
571 layout.topUITotalHeightPx = std::round((UI_TAB_BAR_HEIGHT_MM + UI_DIVIDER_GAP_PX +
572 UI_ACTION_GROUP_LABEL_HEIGHT_MM + UI_DIVIDER_GAP_PX +
573 UI_ACTION_GROUP_HEIGHT_MM + UI_DIVIDER_GAP_PX +
574 UI_ACTION_GROUP_LABEL_HEIGHT_MM + UI_DIVIDER_GAP_PX) * pixelsPerMMy) + 7.0f;
575
576
577 const float groupNavPaddingPx = 8.0f;
578 const float groupNavGapPx = 2.0f * pixelsPerMMx;
579 float groupNavX = 0.0f;
580 for (size_t i = 0; i < TotalTopUIActionGroups; ++i) {
581 const char32_t* label = LocalizedUIString(topUIActionGroupNames[i].labelStringID);
582 layout.actionGroups[i].navX = groupNavX;
583 layout.actionGroups[i].navWidth = std::ceil(MeasureUIStringWidth(label, layout.uiTextScale) +
584 2.0f * groupNavPaddingPx);
585 groupNavX += layout.actionGroups[i].navWidth + groupNavGapPx;
586 }
587 layout.actionGroupNavTotalWidthPx = groupNavX > 0.0f ? groupNavX - groupNavGapPx : 0.0f;
588
589 std::array<bool, TotalTopUIActionGroups> groupSeen{};
590 float currentX = 5.0f;
591 float verticalSlotMaxSize = 0.0f;
592 float contentRight = currentX;
593 int activeSubGroupIndex = -1;
594 size_t activeSubGroupRun = 0;
595
596 for (size_t i = 0; i < TotalUIControls; ++i) {
597 const UIControlDefinition& ctrl = AllUIControls[i];
598 const size_t groupIndex = ctrl.actionGroupIndex;
599 const size_t subGroupIndex = ctrl.actionSubGroupIndex;
600
601 if (groupIndex < TotalTopUIActionGroups && !groupSeen[groupIndex]) {
602 layout.actionGroups[groupIndex].contentStartX = currentX;
603 layout.actionGroups[groupIndex].contentEndX = currentX;
604 groupSeen[groupIndex] = true;
605 }
606
607 if (subGroupIndex < TotalTopUIActionSubGroups &&
608 !layout.actionSubGroups[subGroupIndex].hasControls) {
609 layout.actionSubGroups[subGroupIndex].contentStartX = currentX;
610 layout.actionSubGroups[subGroupIndex].contentEndX = currentX;
611 layout.actionSubGroups[subGroupIndex].hasControls = true;
612 }
613
614 if (activeSubGroupIndex != (int)subGroupIndex &&
615 layout.actionSubGroupRunCount < layout.actionSubGroupRuns.size()) {
616 activeSubGroupIndex = (int)subGroupIndex;
617 activeSubGroupRun = layout.actionSubGroupRunCount++;
618 layout.actionSubGroupRuns[activeSubGroupRun].subGroupIndex = ctrl.actionSubGroupIndex;
619 layout.actionSubGroupRuns[activeSubGroupRun].contentStartX = currentX;
620 layout.actionSubGroupRuns[activeSubGroupRun].contentEndX = currentX;
621 }
622
623 float btnWidth = layout.buttonWidthPx;
624 float btnY = layout.topActionGroupY;
625 const char32_t* label = LocalizedControlLabel(ctrl);
626 if (ctrl.showText && *label != U'\0') {
627 float contentWidth = layout.textStartOffsetPx +
628 MeasureUIStringWidth(label, layout.uiTextScale) + layout.textEndInsetPx;
629 btnWidth = std::max(btnWidth, contentWidth);
630 }
631 if (ctrl.noOfVerticalSlots > 1) {
632 btnY += ctrl.verticalSlotNo * (layout.buttonHeightPx + 1.0f);
633 }
634
635 layout.controls[i] = { currentX, btnY, btnWidth, layout.buttonHeightPx };
636 verticalSlotMaxSize = std::max(verticalSlotMaxSize, btnWidth);
637
638 const bool endOfColumn = (i + 1 == TotalUIControls) || (AllUIControls[i + 1].verticalSlotNo == 0);
639 if (endOfColumn) {
640 const float columnRight = currentX + verticalSlotMaxSize;
641 if (groupIndex < TotalTopUIActionGroups) {
642 layout.actionGroups[groupIndex].contentEndX =
643 std::max(layout.actionGroups[groupIndex].contentEndX, columnRight);
644 }
645 if (subGroupIndex < TotalTopUIActionSubGroups) {
646 layout.actionSubGroups[subGroupIndex].contentEndX =
647 std::max(layout.actionSubGroups[subGroupIndex].contentEndX, columnRight);
648 }
649 if (layout.actionSubGroupRunCount > 0) {
650 layout.actionSubGroupRuns[activeSubGroupRun].contentEndX =
651 std::max(layout.actionSubGroupRuns[activeSubGroupRun].contentEndX, columnRight);
652 }
653 contentRight = std::max(contentRight, columnRight);
654
655 if (i + 1 < TotalUIControls) {
656 currentX = columnRight + layout.buttonGapPx;
657 }
658 verticalSlotMaxSize = 0.0f;
659 }
660 }
661
662 for (size_t gIdx = 0; gIdx < TotalTopUIActionGroups; ++gIdx) {
663 layout.actionGroups[gIdx].contentWidth = std::max(0.0f, layout.actionGroups[gIdx].contentEndX - layout.actionGroups[gIdx].contentStartX);
664 }
665
666 layout.totalContentWidthPx = contentRight + 30.0f;
667 layout.scrollOffsetPx = previousScroll;
668 layout.isValid = true;
669}
670
671static float MapRibbonToNav(float x, const UITopRibbonLayout& layout) {
672 if (TotalTopUIActionGroups == 0) return 0.0f;
673
674 const auto& firstGroup = layout.actionGroups[0];
675 if (x <= firstGroup.contentStartX) {
676 return firstGroup.navX;
677 }
678 const auto& lastGroup = layout.actionGroups[TotalTopUIActionGroups - 1];
679 if (x >= lastGroup.contentEndX) {
680 return lastGroup.navX + lastGroup.navWidth;
681 }
682
683 for (size_t i = 0; i < TotalTopUIActionGroups; ++i) {
684 const auto& grp = layout.actionGroups[i];
685 if (x >= grp.contentStartX && x <= grp.contentEndX) {
686 float width = grp.contentWidth;
687 if (width <= 0.0f) return grp.navX;
688 float t = (x - grp.contentStartX) / width;
689 return grp.navX + t * grp.navWidth;
690 }
691 if (i + 1 < TotalTopUIActionGroups) {
692 const auto& nextGrp = layout.actionGroups[i + 1];
693 if (x > grp.contentEndX && x < nextGrp.contentStartX) {
694 float gapWidth = nextGrp.contentStartX - grp.contentEndX;
695 if (gapWidth <= 0.0f) return nextGrp.navX;
696 float t = (x - grp.contentEndX) / gapWidth;
697 float startNav = grp.navX + grp.navWidth;
698 float endNav = nextGrp.navX;
699 return startNav + t * (endNav - startNav);
700 }
701 }
702 }
703
704 return 0.0f;
705}
706
707// PushRect
708void PushRect( UIDrawContext& ctx, float x, float y, float w, float h,
709 uint32_t color, DX12ResourcesUI& uiRes) {
710 if (ctx.vertexCount + 4 > uiRes.maxVertices) return;
711 if (ctx.indexCount + 6 > uiRes.maxIndices) return;
712
713 uint16_t base = ctx.vertexCount;
714
715 ctx.vertexPtr[0] = { x,y,0,0,color, UI_ENGLISH_ATLAS_SLOT };
716 ctx.vertexPtr[1] = { x + w,y,0,0,color, UI_ENGLISH_ATLAS_SLOT };
717 ctx.vertexPtr[2] = { x + w,y + h,0,0,color, UI_ENGLISH_ATLAS_SLOT };
718 ctx.vertexPtr[3] = { x,y + h,0,0,color, UI_ENGLISH_ATLAS_SLOT };
719
720 ctx.indexPtr[0] = base + 0;
721 ctx.indexPtr[1] = base + 1;
722 ctx.indexPtr[2] = base + 2;
723 ctx.indexPtr[3] = base + 0;
724 ctx.indexPtr[4] = base + 2;
725 ctx.indexPtr[5] = base + 3;
726}
727
728static void PushTexturedQuad(UIDrawContext& ctx, float x, float y, float w, float h,
729 const UIAtlasRegion& region, uint32_t atlasIndex, uint32_t color, DX12ResourcesUI& uiRes) {
730 if (ctx.vertexCount + 4 > uiRes.maxVertices) return;
731 if (ctx.indexCount + 6 > uiRes.maxIndices) return;
732
733 uint16_t base = ctx.vertexCount;
734 ctx.vertexPtr[0] = { x, y, region.uvMinX, region.uvMinY, color, atlasIndex };
735 ctx.vertexPtr[1] = { x + w, y, region.uvMaxX, region.uvMinY, color, atlasIndex };
736 ctx.vertexPtr[2] = { x + w, y + h, region.uvMaxX, region.uvMaxY, color, atlasIndex };
737 ctx.vertexPtr[3] = { x, y + h, region.uvMinX, region.uvMaxY, color, atlasIndex };
738
739 ctx.indexPtr[0] = base + 0;
740 ctx.indexPtr[1] = base + 1;
741 ctx.indexPtr[2] = base + 2;
742 ctx.indexPtr[3] = base + 0;
743 ctx.indexPtr[4] = base + 2;
744 ctx.indexPtr[5] = base + 3;
745
746 ctx.vertexPtr += 4;
747 ctx.indexPtr += 6;
748 ctx.vertexCount += 4;
749 ctx.indexCount += 6;
750}
751
752void PushRoundedRectangle(UIDrawContext& ctx, float x, float y, float w, float h, float radiusPx,
753 uint32_t color, DX12ResourcesUI& uiRes) {
754 if (w <= 0.0f || h <= 0.0f) return;
755 if (ctx.vertexCount + 36 > uiRes.maxVertices) return;
756 if (ctx.indexCount + 54 > uiRes.maxIndices) return;
757
758 const float clampedRadius = std::max(1.0f, std::min(radiusPx, 0.5f * std::min(w, h)));
759 const float xCuts[4] = { x, x + clampedRadius, x + w - clampedRadius, x + w };
760 const float yCuts[4] = { y, y + clampedRadius, y + h - clampedRadius, y + h };
761
762 /*
763 for (int row = 0; row < 3; ++row) {
764 for (int col = 0; col < 3; ++col) {
765 PushTexturedQuad(ctx, xCuts[col], yCuts[row],
766 xCuts[col + 1] - xCuts[col], yCuts[row + 1] - yCuts[row],
767 gIconAtlasMetadata.roundedRectangle.regions[row][col],
768 UI_ICON_ATLAS_SLOT, color, uiRes);
769 }
770 }*/
771 // Unrolled the above loops for better performance (fewer function calls, better instruction-level parallelism)
772 PushTexturedQuad(ctx, xCuts[0], yCuts[0], xCuts[1] - xCuts[0], yCuts[1] - yCuts[0],
773 gIconAtlasMetadata.roundedRectangle.regions[0][0], UI_ICON_ATLAS_SLOT, color, uiRes);
774 PushTexturedQuad(ctx, xCuts[1], yCuts[0], xCuts[2] - xCuts[1], yCuts[1] - yCuts[0],
775 gIconAtlasMetadata.roundedRectangle.regions[0][1], UI_ICON_ATLAS_SLOT, color, uiRes);
776 PushTexturedQuad(ctx, xCuts[2], yCuts[0], xCuts[3] - xCuts[2], yCuts[1] - yCuts[0],
777 gIconAtlasMetadata.roundedRectangle.regions[0][2], UI_ICON_ATLAS_SLOT, color, uiRes);
778 PushTexturedQuad(ctx, xCuts[0], yCuts[1], xCuts[1] - xCuts[0], yCuts[2] - yCuts[1],
779 gIconAtlasMetadata.roundedRectangle.regions[1][0], UI_ICON_ATLAS_SLOT, color, uiRes);
780 PushTexturedQuad(ctx, xCuts[1], yCuts[1], xCuts[2] - xCuts[1], yCuts[2] - yCuts[1],
781 gIconAtlasMetadata.roundedRectangle.regions[1][1], UI_ICON_ATLAS_SLOT, color, uiRes);
782 PushTexturedQuad(ctx, xCuts[2], yCuts[1], xCuts[3] - xCuts[2], yCuts[2] - yCuts[1],
783 gIconAtlasMetadata.roundedRectangle.regions[1][2], UI_ICON_ATLAS_SLOT, color, uiRes);
784 PushTexturedQuad(ctx, xCuts[0], yCuts[2], xCuts[1] - xCuts[0], yCuts[3] - yCuts[2],
785 gIconAtlasMetadata.roundedRectangle.regions[2][0], UI_ICON_ATLAS_SLOT, color, uiRes);
786 PushTexturedQuad(ctx, xCuts[1], yCuts[2], xCuts[2] - xCuts[1], yCuts[3] - yCuts[2],
787 gIconAtlasMetadata.roundedRectangle.regions[2][1], UI_ICON_ATLAS_SLOT, color, uiRes);
788 PushTexturedQuad(ctx, xCuts[2], yCuts[2], xCuts[3] - xCuts[2], yCuts[3] - yCuts[2],
789 gIconAtlasMetadata.roundedRectangle.regions[2][2], UI_ICON_ATLAS_SLOT, color, uiRes);
790}
791
792void PushTopRoundedRectangle(UIDrawContext& ctx, float x, float y, float w, float h, float radiusPx,
793 uint32_t color, DX12ResourcesUI& uiRes) {
794 if (w <= 0.0f || h <= 0.0f) return;
795 if (ctx.vertexCount + 24 > uiRes.maxVertices) return;
796 if (ctx.indexCount + 36 > uiRes.maxIndices) return;
797
798 const float clampedRadius = std::max(1.0f, std::min(radiusPx, 0.5f * std::min(w, h)));
799 const float xCuts[4] = { x, x + clampedRadius, x + w - clampedRadius, x + w };
800 const float yCuts[3] = { y, y + clampedRadius, y + h };
801
802 // Row 0 (Top part with rounded corners)
803 PushTexturedQuad(ctx, xCuts[0], yCuts[0], xCuts[1] - xCuts[0], yCuts[1] - yCuts[0],
804 gIconAtlasMetadata.roundedRectangle.regions[0][0], UI_ICON_ATLAS_SLOT, color, uiRes);
805 PushTexturedQuad(ctx, xCuts[1], yCuts[0], xCuts[2] - xCuts[1], yCuts[1] - yCuts[0],
806 gIconAtlasMetadata.roundedRectangle.regions[0][1], UI_ICON_ATLAS_SLOT, color, uiRes);
807 PushTexturedQuad(ctx, xCuts[2], yCuts[0], xCuts[3] - xCuts[2], yCuts[1] - yCuts[0],
808 gIconAtlasMetadata.roundedRectangle.regions[0][2], UI_ICON_ATLAS_SLOT, color, uiRes);
809
810 // Row 1 (Bottom part with sharp corners utilizing the flat middle row)
811 PushTexturedQuad(ctx, xCuts[0], yCuts[1], xCuts[1] - xCuts[0], yCuts[2] - yCuts[1],
812 gIconAtlasMetadata.roundedRectangle.regions[1][0], UI_ICON_ATLAS_SLOT, color, uiRes);
813 PushTexturedQuad(ctx, xCuts[1], yCuts[1], xCuts[2] - xCuts[1], yCuts[2] - yCuts[1],
814 gIconAtlasMetadata.roundedRectangle.regions[1][1], UI_ICON_ATLAS_SLOT, color, uiRes);
815 PushTexturedQuad(ctx, xCuts[2], yCuts[1], xCuts[3] - xCuts[2], yCuts[2] - yCuts[1],
816 gIconAtlasMetadata.roundedRectangle.regions[1][2], UI_ICON_ATLAS_SLOT, color, uiRes);
817}
818
819static void PushIcon(UIDrawContext& ctx, float x, float y, float w, float h, char32_t iconCodepoint,
820 uint32_t color, DX12ResourcesUI& uiRes) {
821 auto iconIt = iconGlyphLookup.find(iconCodepoint);
822 if (iconIt == iconGlyphLookup.end()) return;
823 const Glyph& glyph = iconIt->second;
824 UIAtlasRegion icon{ glyph.uvMinX, glyph.uvMinY, glyph.uvMaxX, glyph.uvMaxY };
825 PushTexturedQuad(ctx, x, y, w, h, icon, UI_ICON_ATLAS_SLOT, color, uiRes);
826}
827
828// Returns true if clicked this frame
829bool PushInteractiveRect(UIDrawContext& ctx, float x, float y, float w, float h, uint32_t baseColor,
830 uint32_t id, const UIInput& input, DX12ResourcesUI& uiRes, bool enabled = true) {
831 uint32_t color = baseColor;
832
833 bool hovered = enabled && (input.mouseX >= x && input.mouseX < x + w &&
834 input.mouseY >= y && input.mouseY < y + h);
835
836 if (hovered) color = 0xFF555555; // hover tint (TODO: theme-aware)
837 if (hovered && input.leftButtonDown) color = 0xFF333333; // pressed tint
838 if (!enabled) color = 0xFF1E1E1E; // If disabled, force a darker/grayer base color
839
840 PushRect(ctx, x, y, w, h, color, uiRes);
841
842 if (!enabled) return false;// Disabled controls do NOT respond to clicks
843 if (hovered && input.leftButtonPressedThisFrame) {
844 return true;
845 }
846 return false;
847}
848
849void PushText(UIDrawContext& ctx, float x, float y, const char* text, uint32_t color, DX12ResourcesUI& uiRes)
850{
851 float cursorX = x;
852 uint32_t glyphCount = 0;
853
854 for (const char* p = text; *p; ++p)
855 {
856 // Bounds Checking (Crucial for text strings)
857 if (ctx.vertexCount + (glyphCount + 1) * 4 > uiRes.maxVertices) return;
858 if (ctx.indexCount + (glyphCount + 1) * 6 > uiRes.maxIndices) return;
859
860 char c = *p;
861 if (glyphLookup.find(c) == glyphLookup.end()) continue;
862
863 const Glyph& g = glyphLookup[c];
864 if (g.width <= 0 || g.height <= 0) {
865 cursorX += g.advanceX;
866 continue;
867 }
868
869 float xpos = cursorX + g.bearingX;
870 float ypos = y - g.bearingY;
871 float w = (float)g.width;
872 float h = (float)g.height;
873 uint32_t vertexOffset = glyphCount * 4;
874 uint32_t indexOffset = glyphCount * 6;
875
876 // Add 4 vertices. Write relative to the current pointer (0, 1, 2, 3)
877 uint32_t vidx = ctx.vertexCount + vertexOffset;
878 ctx.vertexPtr[vertexOffset + 0] = { xpos, ypos, g.uvMinX, g.uvMinY, color, UI_ENGLISH_ATLAS_SLOT };
879 ctx.vertexPtr[vertexOffset + 1] = { xpos + w, ypos, g.uvMaxX, g.uvMinY, color, UI_ENGLISH_ATLAS_SLOT };
880 ctx.vertexPtr[vertexOffset + 2] = { xpos + w, ypos + h, g.uvMaxX, g.uvMaxY, color, UI_ENGLISH_ATLAS_SLOT };
881 ctx.vertexPtr[vertexOffset + 3] = { xpos, ypos + h, g.uvMinX, g.uvMaxY, color, UI_ENGLISH_ATLAS_SLOT };
882
883 // Add 6 indices. Write indices relative to the current index pointer
884 ctx.indexPtr[indexOffset + 0] = vidx + 0;
885 ctx.indexPtr[indexOffset + 1] = vidx + 1;
886 ctx.indexPtr[indexOffset + 2] = vidx + 2;
887 ctx.indexPtr[indexOffset + 3] = vidx + 0;
888 ctx.indexPtr[indexOffset + 4] = vidx + 2;
889 ctx.indexPtr[indexOffset + 5] = vidx + 3;
890
891 glyphCount++;
892 cursorX += g.advanceX;
893 }
894
895 ctx.vertexPtr += glyphCount * 4;
896 ctx.indexPtr += glyphCount * 6;
897 ctx.vertexCount += glyphCount * 4;
898 ctx.indexCount += glyphCount * 6;
899}
900
901// This function renders the list of tabs, all top menu buttons (with dropdowns if required),
902// side favorite / frequent buttons bars, right side property window, bottom status bar.
903// This is also responsible for all relevant DirectX12 configurations required for rendering User Interface.
904void RenderUIOverlay(SingleUIWindow& window, ID3D12GraphicsCommandList* cmd, DX12ResourcesUI& uiRes,
905 UITopRibbonLayout& topRibbonLayout, float monitorDPIX, float monitorDPIY, const UIInput& input) {
906
907 if (!cmd) return; //Defensive check.
908
909 cmd->SetPipelineState(uiRes.uiPSO.Get());
910 cmd->SetGraphicsRootSignature(uiRes.uiRootSignature.Get());
911
912 // Bind descriptor heap
913 ID3D12DescriptorHeap* heaps[] = { uiRes.srvHeap.Get(), uiRes.samplerHeap.Get() };
914 cmd->SetDescriptorHeaps(_countof(heaps), heaps);
915
916 // Bind the descriptor table (which contains t0 + s0)
917 // Root Parameter 1 = SRV table// must match rootParams[1]
918 cmd->SetGraphicsRootDescriptorTable(1, uiRes.srvHeap->GetGPUDescriptorHandleForHeapStart());
919 // Root Parameter 2 = Sampler table
920 cmd->SetGraphicsRootDescriptorTable(2, uiRes.samplerHeap->GetGPUDescriptorHandleForHeapStart());
921 // Bind ortho constant buffer (still root parameter 0)
922 cmd->SetGraphicsRootConstantBufferView(0, uiRes.uiOrthoConstantBuffer->GetGPUVirtualAddress());
923
924 float W = (float)window.dx.WindowWidth;
925 float H = (float)window.dx.WindowHeight;
926 float* ortho = (float*)uiRes.pOrthoDataBegin;
927
928 ortho[0] = 2 / W; ortho[1] = 0; ortho[2] = 0; ortho[3] = -1;
929 ortho[4] = 0; ortho[5] = -2 / H; ortho[6] = 0; ortho[7] = 1;
930 ortho[8] = 0; ortho[9] = 0; ortho[10] = 1; ortho[11] = 0;
931 ortho[12] = 0; ortho[13] = 0; ortho[14] = 0; ortho[15] = 1;
932
933 cmd->SetGraphicsRootConstantBufferView( 0, uiRes.uiOrthoConstantBuffer->GetGPUVirtualAddress());
934
935 UIDrawContext ctx;
936 ctx.vertexPtr = reinterpret_cast<UIVertex*>(uiRes.pVertexDataBegin);
937 ctx.indexPtr = reinterpret_cast<uint16_t*>(uiRes.pIndexDataBegin);
938 ctx.vertexCount = 0;
939 ctx.indexCount = 0;
940 if (!topRibbonLayout.isValid || topRibbonLayout.dpiX != monitorDPIX || topRibbonLayout.dpiY != monitorDPIY) {
941 PrecomputeTopRibbonLayout(topRibbonLayout, monitorDPIX, monitorDPIY);
942 }
943 if (input.mouseWheelDelta != 0 && input.mouseY >= 0.0f && input.mouseY < topRibbonLayout.topUITotalHeightPx) {
944 const float wheelSteps = input.mouseWheelDelta / (float)WHEEL_DELTA;
945 const float scrollStepPx = std::max(topRibbonLayout.buttonWidthPx * 2.0f, 120.0f);
946 topRibbonLayout.scrollOffsetPx -= wheelSteps * scrollStepPx;
947 }
948 ClampTopRibbonScroll(topRibbonLayout, W);
949
950 float pixelsPerMMx = monitorDPIX / 25.4f;
951 float pixelsPerMMy = monitorDPIY / 25.4f;
952 float iconSizePx = topRibbonLayout.iconSizePx;
953 float buttonHeightPx = topRibbonLayout.buttonHeightPx;
954 float iconReservedWidthPx = topRibbonLayout.iconReservedWidthPx;
955 float textStartOffsetPx = topRibbonLayout.textStartOffsetPx;
956 float textEndInsetPx = topRibbonLayout.textEndInsetPx;
957 float tabBarHeightPx = topRibbonLayout.tabBarHeightPx;
958 float topUITotalHeightPx = topRibbonLayout.topUITotalHeightPx;
959 float roundedCornerRadiusPx = topRibbonLayout.roundedCornerRadiusPx;
960
961 auto canPushRect = [&]() {
962 return ctx.vertexCount + 4 <= uiRes.maxVertices &&
963 ctx.indexCount + 6 <= uiRes.maxIndices;
964 };
965
966 auto incrementVertexIndexCounters = [&]() {
967 ctx.vertexPtr += 4;
968 ctx.indexPtr += 6;
969 ctx.vertexCount += 4;
970 ctx.indexCount += 6;
971 };
972
973 auto pushRect = [&](float x, float y, float w, float h, uint32_t color) {
974 bool pushed = canPushRect();
975 PushRect(ctx, x, y, w, h, color, uiRes);
976 if (pushed) incrementVertexIndexCounters();
977 };
978
979 const float uiTextScale = topRibbonLayout.uiTextScale;
980
981 auto pushTextClipped = [&](float x, float y, const char32_t* text, float maxWidth, uint32_t color,
982 float scale) {
983 if (!text || maxWidth <= 0.0f) return;
984
985 float cursorX = x;
986 float textRight = x + maxWidth;
987
988 for (const char32_t* p = text; *p; ++p) {
989 if (*p > 0x7F) continue;
990
991 auto glyphIt = glyphLookup.find(*p);
992 if (glyphIt == glyphLookup.end()) continue;
993
994 const Glyph& g = glyphIt->second;
995 if (g.width <= 0 || g.height <= 0) {
996 cursorX += (float)g.advanceX * scale;
997 continue;
998 }
999
1000 // It is always better to be aligned to pixels for better text clarity.
1001 float xpos = std::floor(cursorX + (float)g.bearingX * scale + 0.5f);
1002 float ypos = std::floor(y - (float)g.bearingY * scale + 0.5f);
1003 float glyphWidth = (float)g.width * scale;
1004 float glyphHeight = (float)g.height * scale;
1005 float glyphRight = xpos + glyphWidth;
1006
1007 if (glyphRight > textRight) break;
1008 if (ctx.vertexCount + 4 > uiRes.maxVertices) return;
1009 if (ctx.indexCount + 6 > uiRes.maxIndices) return;
1010
1011 uint16_t base = ctx.vertexCount;
1012 ctx.vertexPtr[0] = { xpos, ypos, g.uvMinX, g.uvMinY, color, UI_ENGLISH_ATLAS_SLOT };
1013 ctx.vertexPtr[1] = { xpos + glyphWidth, ypos, g.uvMaxX, g.uvMinY, color, UI_ENGLISH_ATLAS_SLOT };
1014 ctx.vertexPtr[2] = { xpos + glyphWidth, ypos + glyphHeight, g.uvMaxX, g.uvMaxY, color, UI_ENGLISH_ATLAS_SLOT };
1015 ctx.vertexPtr[3] = { xpos, ypos + glyphHeight, g.uvMinX, g.uvMaxY, color, UI_ENGLISH_ATLAS_SLOT };
1016
1017 ctx.indexPtr[0] = base + 0;
1018 ctx.indexPtr[1] = base + 1;
1019 ctx.indexPtr[2] = base + 2;
1020 ctx.indexPtr[3] = base + 0;
1021 ctx.indexPtr[4] = base + 2;
1022 ctx.indexPtr[5] = base + 3;
1023
1024 ctx.vertexPtr += 4;
1025 ctx.indexPtr += 6;
1026 ctx.vertexCount += 4;
1027 ctx.indexCount += 6;
1028 cursorX += (float)g.advanceX * scale;
1029 }
1030 };
1031
1032 auto textBaselineY = [&](float y, float h, float scale) {
1033 auto glyphIt = glyphLookup.find(U'M');
1034 if (glyphIt == glyphLookup.end()) return y + h * 0.7f;
1035
1036 const Glyph& g = glyphIt->second;
1037 return y + h * 0.5f + (float)g.bearingY * scale - (float)g.height * scale * 0.5f;
1038 };
1039
1040 // ENGINEERING / PROJECT TABs
1041 // Action ids for engineering thread control (UI -> engineering)
1042 constexpr uint32_t ACTION_ENGINEERING_CLOSE = 0xE0000001u;
1043 constexpr uint32_t ACTION_ENGINEERING_CREATE = 0xE0000002u;
1044
1045 float currentX = 0.0f;
1046
1047 uint16_t tabCount = publishedTabCount.load(std::memory_order_acquire);
1048 uint16_t* tabList = publishedTabIndexes.load(std::memory_order_acquire);
1049
1050 if (canPushRect()) {
1051 PushRect(ctx, 0.0f, 0.0f, 5000.0f, topUITotalHeightPx, uiActiveColors.actionGroupBackground, uiRes);//
1052 incrementVertexIndexCounters();
1053 }
1054
1055 if (canPushRect()) {
1056 PushRect(ctx, 0.0f, 0.0f, 5000.0f, tabBarHeightPx, uiActiveColors.tabBackground, uiRes);//
1057 incrementVertexIndexCounters();
1058 }
1059 // We will allow tabs to shrink progressively when too many tabs exist.
1060 // Compute sizing constraints
1061 const float defaultTabWidth = 160.0f; // legacy fixed width in pixels
1062 const float plusButtonWidth = buttonHeightPx; // reserve square area for '+'
1063 const float minTabWidth = std::max(4.0f * pixelsPerMMx, 8.0f); // 4mm minimum as requested, but at least 8px
1064
1065 // Determine how many slots we need to fit: tabs + one slot for '+' button
1066 uint16_t slotsNeeded = tabCount + 1;
1067 float availableForTabs = std::max(0.0f, W - plusButtonWidth);
1068
1069 float tentativeWidth = availableForTabs / (float)slotsNeeded;
1070 float tabWidthPx = defaultTabWidth;
1071 uint16_t visibleTabs = tabCount;
1072
1073 if (tentativeWidth >= defaultTabWidth) {
1074 tabWidthPx = defaultTabWidth;
1075 } else if (tentativeWidth >= minTabWidth) {
1076 tabWidthPx = tentativeWidth;
1077 } else {
1078 // If tentative width is below minimum, we must hide some tabs.
1079 visibleTabs = (uint16_t)std::floor(availableForTabs / minTabWidth);
1080 if (visibleTabs > tabCount) visibleTabs = tabCount;
1081 tabWidthPx = minTabWidth;
1082 }
1083
1084 // Render visible tabs only; hidden tabs are not drawn (will be handled by horizontal scroll in future)
1085 // Gap between tabs: 0.5 mm on either side
1086 float gapPx = 0.5f * pixelsPerMMx;
1087 for (uint16_t i = 0; i < visibleTabs; i++) {
1088 uint16_t tabID = tabList[i];
1089 bool isActive = (window.activeTabIndex == tabID);
1090
1091 // area for this tab (slot)
1092 float tabX = currentX;
1093 float tabW = tabWidthPx;
1094
1095 // content area inset by half-mm gaps on either side
1096 float contentX = tabX + gapPx;
1097 float contentW = std::max(0.0f, tabW - 2.0f * gapPx);
1098
1099 // X (close) button sizing — square inside tab on the right
1100 float xBtnSize = std::round(std::min(tabW * 0.5f, std::max( (float)std::round(UI_ICON_SIZE_MM * pixelsPerMMx), 10.0f)));
1101 if (xBtnSize + 4.0f > tabW) xBtnSize = std::max(4.0f, tabW - 4.0f);
1102 float xBtnX = tabX + tabW - xBtnSize - 4.0f;
1103 float xBtnY = std::floor((tabBarHeightPx - xBtnSize) * 0.5f);
1104
1105 // Entire tab background — draw only inside content area leaving gaps between tabs
1106 if (isActive) {
1107 PushTopRoundedRectangle(ctx, contentX, 0.0f, contentW, tabBarHeightPx, roundedCornerRadiusPx, uiActiveColors.actionGroupBackground, uiRes);
1108 } else {
1109 bool pushed = canPushRect();
1110 PushRect(ctx, contentX, 0.0f, contentW, tabBarHeightPx, uiActiveColors.tabBackground, uiRes);
1111 if (pushed) incrementVertexIndexCounters();
1112 }
1113
1114 // Check clicks on the X button first to avoid activating the tab when user intends to close
1115 bool xHovered = input.mouseX >= xBtnX && input.mouseX < xBtnX + xBtnSize &&
1116 input.mouseY >= xBtnY && input.mouseY < xBtnY + xBtnSize;
1117 if (xHovered && input.leftButtonPressedThisFrame) {
1118 // Signal close intent to engineering thread. Pass tabID in parameter p1.
1119 PushUIAction(ACTION_ENGINEERING_CLOSE, (uint32_t)tabID, 0);
1120 }
1121
1122 // If user clicked on non-X area of tab, activate it
1123 bool tabHovered = input.mouseX >= tabX && input.mouseX < tabX + tabW &&
1124 input.mouseY >= 0 && input.mouseY < tabBarHeightPx;
1125 if (!xHovered && tabHovered && input.leftButtonPressedThisFrame) {
1126 window.activeTabIndex = tabID; // Render thread will draw this tab's geometry on the next frame.
1127 }
1128
1129 // Draw the X button: only draw rounded background when hovered, otherwise render as plain text
1130 char32_t xChar[2] = { U'x', U'\0' };
1131 if (xHovered) {
1132 PushRoundedRectangle(ctx, xBtnX, xBtnY, xBtnSize, xBtnSize, std::max(1.0f, roundedCornerRadiusPx * 0.6f),
1133 0xFF444444, uiRes);
1134 pushTextClipped(xBtnX + 2.0f, textBaselineY(xBtnY, xBtnSize, uiTextScale), xChar, xBtnSize - 4.0f, 0xFFFFFFFF, uiTextScale);
1135 } else {
1136 // Render as plain small text matching tab text color
1137 pushTextClipped(xBtnX + 2.0f, textBaselineY(xBtnY, xBtnSize, uiTextScale), xChar, xBtnSize - 4.0f, uiActiveColors.tabBackgroundText, uiTextScale);
1138 }
1139
1140 // Draw label clipped to remaining area (avoid overlapping with X)
1141 std::u32string tabLabel;
1142 tabLabel.reserve(allTabs[tabID].fileName.size());
1143 for (wchar_t ch : allTabs[tabID].fileName) {
1144 if (ch <= 0x7F) tabLabel.push_back(static_cast<char32_t>(ch));
1145 }
1146
1147 float labelMaxWidth = contentW - (8.0f + xBtnSize + 4.0f);
1148 pushTextClipped(contentX + 8.0f, textBaselineY(0.0f, tabBarHeightPx, uiTextScale), tabLabel.c_str(), labelMaxWidth, uiActiveColors.tabBackgroundText, uiTextScale);
1149
1150 currentX += tabW;
1151
1152 // Draw 1px vertical separator centered in the gap between tabs (only between tabs)
1153 if (i + 1 < visibleTabs) {
1154 float sepX = tabX + tabW; // center of gap between this tab and next
1155 // align to pixel for crispness
1156 float sepXi = std::floor(sepX + 0.5f);
1157 pushRect(sepXi, 2.0f, 1.0f, tabBarHeightPx - 4.0f, uiActiveColors.actionGroupSeperator);
1158 }
1159 }
1160
1161 // If some tabs are hidden, we may draw a subtle indicator (ellipsis) — skip for now
1162
1163 // Render '+' create new thread button at the end of tab bar
1164 float plusX = currentX + 6.0f; // small padding before plus
1165 float plusSize = std::max(plusButtonWidth, std::round(UI_ICON_SIZE_MM * pixelsPerMMy) + 8.0f);
1166 bool plusHovered = input.mouseX >= plusX && input.mouseX < plusX + plusSize &&
1167 input.mouseY >= (tabBarHeightPx - plusSize) * 0.5f && input.mouseY < (tabBarHeightPx - plusSize) * 0.5f + plusSize;
1168 // '+' button: show rounded background only on hover; otherwise render as plain icon/text
1169 if (plusHovered) {
1170 PushRoundedRectangle(ctx, plusX, (tabBarHeightPx - plusSize) * 0.5f, plusSize, plusSize, roundedCornerRadiusPx,
1171 0xFF444444, uiRes);
1172 if (!gIconAtlasMetadata.mixedIconCodepoints.empty()) {
1173 PushIcon(ctx, plusX + (plusSize - iconSizePx) * 0.5f, (tabBarHeightPx - iconSizePx) * 0.5f,
1174 iconSizePx, iconSizePx, gIconAtlasMetadata.mixedIconCodepoints[0], 0xFFFFFFFF, uiRes);
1175 }
1176 } else {
1177 if (!gIconAtlasMetadata.mixedIconCodepoints.empty()) {
1178 PushIcon(ctx, plusX + (plusSize - iconSizePx) * 0.5f, (tabBarHeightPx - iconSizePx) * 0.5f,
1179 iconSizePx, iconSizePx, gIconAtlasMetadata.mixedIconCodepoints[0], uiActiveColors.tabBackgroundText, uiRes);
1180 }
1181 }
1182 if (plusHovered && input.leftButtonPressedThisFrame) {
1183 PushUIAction(ACTION_ENGINEERING_CREATE, 0, 0);
1184 }
1185
1186 // TOP BUTTONS (ACTION GROUP BAR)
1187 const float buttonGap = topRibbonLayout.buttonGapPx;
1188 const float actionGroupLabelY = topRibbonLayout.actionGroupLabelY;
1189 const float groupLabelHeight = topRibbonLayout.actionGroupLabelHeightPx;
1190 const float topActionGroupY = topRibbonLayout.topActionGroupY;
1191 const float actionSubGroupLabelY = topRibbonLayout.actionSubGroupLabelY;
1192 const float ribbonScrollX = topRibbonLayout.scrollOffsetPx;
1193
1194 // Draw the 5-pixel high extent-of-ribbon-visible visualization bar in the 5px gap below Action Group labels.
1195 // The gap starts at topActionGroupY - 5.0f.
1196 float extentX = MapRibbonToNav(topRibbonLayout.scrollOffsetPx, topRibbonLayout);
1197 float extentRight = MapRibbonToNav(topRibbonLayout.scrollOffsetPx + W, topRibbonLayout);
1198 float extentW = std::max(1.0f, extentRight - extentX);
1199 // Draw active indicator (orange)
1200 pushRect(extentX, topActionGroupY - 5.0f, extentW, 5.0f, 0xFF3399FF);
1201
1202 for (size_t groupIndex = 0; groupIndex < TotalTopUIActionGroups; ++groupIndex) {
1203 const UIActionGroupNames& group = topUIActionGroupNames[groupIndex];
1204 const UITopRibbonActionGroupLayout& groupLayout = topRibbonLayout.actionGroups[groupIndex];
1205 const char32_t* label = LocalizedUIString(group.labelStringID);
1206 const bool hovered = group.isEnabled &&
1207 input.mouseX >= groupLayout.navX && input.mouseX < groupLayout.navX + groupLayout.navWidth &&
1208 input.mouseY >= actionGroupLabelY && input.mouseY < actionGroupLabelY + groupLabelHeight;
1209
1210 if (hovered) {
1211 pushRect(groupLayout.navX, actionGroupLabelY, groupLayout.navWidth, groupLabelHeight,
1212 uiActiveColors.tabBackgroundHover);
1213 }
1214 if (hovered && input.leftButtonPressedThisFrame) {
1215 topRibbonLayout.scrollOffsetPx = groupLayout.contentStartX;
1216 ClampTopRibbonScroll(topRibbonLayout, W);
1217 }
1218
1219 pushTextClipped(groupLayout.navX + 4.0f, textBaselineY(actionGroupLabelY, groupLabelHeight, uiTextScale),
1220 label, groupLayout.navWidth - 8.0f, uiActiveColors.actionText, uiTextScale);
1221 }
1222
1223 for (size_t runIndex = 0; runIndex < topRibbonLayout.actionSubGroupRunCount; ++runIndex) {
1224 const UITopRibbonSubGroupRunLayout& run = topRibbonLayout.actionSubGroupRuns[runIndex];
1225 if (run.subGroupIndex >= TotalTopUIActionSubGroups) continue;
1226
1227 const UIActionGroupNames& subGroup = topUIActionSubGroupNames[run.subGroupIndex];
1228 const char32_t* label = LocalizedUIString(subGroup.labelStringID);
1229 const float runX = run.contentStartX - ribbonScrollX;
1230 const float runWidth = std::max(0.0f, run.contentEndX - run.contentStartX);
1231 const float labelWidth = MeasureUIStringWidth(label, uiTextScale);
1232 const float labelX = runX + std::max(4.0f, (runWidth - labelWidth) * 0.5f);
1233
1234 pushTextClipped(labelX, textBaselineY(actionSubGroupLabelY, groupLabelHeight, uiTextScale),
1235 label, std::max(0.0f, runWidth - 8.0f), uiActiveColors.actionText, uiTextScale);
1236
1237 if (runIndex + 1 < topRibbonLayout.actionSubGroupRunCount) {
1238 const float lineX = std::floor(run.contentEndX + buttonGap * 0.5f - ribbonScrollX);
1239 const float lineHeight = 3.0f * topRibbonLayout.buttonHeightPx + 2.0f;
1240 if (lineX >= -1.0f && lineX <= W + 1.0f) {
1241 pushRect(lineX, topActionGroupY, 1.0f, lineHeight, 0xFF555555);
1242 }
1243 }
1244 }
1245
1246 for (size_t i = 0; i < TotalUIControls; ++i) {
1247 const auto& ctrl = AllUIControls[i];
1248 const UITopRibbonControlLayout& ctrlLayout = topRibbonLayout.controls[i];
1249 const float btnX = ctrlLayout.x - ribbonScrollX;
1250 const float btnY = ctrlLayout.y;
1251 const float btnWidth = ctrlLayout.width;
1252 const float btnHeight = ctrlLayout.height;
1253 const char32_t* label = LocalizedControlLabel(ctrl);
1254 uint32_t baseColor = StableRandomUIColour((uint32_t)ctrl.action ^ ((uint32_t)i * 0x9E3779B9u));// Render
1255 uint32_t iconColor = StableRandomUIColour(((uint32_t)ctrl.action << 1) ^ 0xA511E9B3u ^ (uint32_t)i);
1256 const bool controlVisible = btnX + btnWidth >= 0.0f && btnX <= W;
1257 bool hovered = false;
1258
1259 if (ctrl.type == 1 || ctrl.type == 2) { // Button or Dropdown trigger
1260 hovered = controlVisible && ctrl.isEnabled && (input.mouseX >= btnX && input.mouseX < btnX + btnWidth &&
1261 input.mouseY >= btnY && input.mouseY < btnY + btnHeight);
1262 uint32_t drawColor = hovered && input.leftButtonDown ? 0xFF333333 : baseColor;
1263 if (hovered && !input.leftButtonDown) drawColor = 0xFF555555;
1264 if (controlVisible) {
1265 if (hovered) {
1266 PushRoundedRectangle(ctx, btnX, btnY, btnWidth, btnHeight, roundedCornerRadiusPx,
1267 drawColor, uiRes);
1268 } else {
1269 float highlightWidth = ctrl.showText ? iconReservedWidthPx : btnWidth;
1270 PushRoundedRectangle(ctx, btnX, btnY, highlightWidth, btnHeight, roundedCornerRadiusPx,
1271 baseColor, uiRes);
1272 }
1273 }
1274
1275 bool clicked = hovered && input.leftButtonPressedThisFrame;
1276
1277 if (clicked && ctrl.isEnabled) {
1278 PushUIAction((uint32_t)ctrl.action);
1279 if (ctrl.zIndex == 1) { // Dropdown trigger
1280 window.activeDropdownAction = ctrl.action;
1281 }
1282 }
1283
1284 if (!ctrl.isEnabled) { // Gray-out overlay for disabled controls
1285 if (controlVisible) {
1286 float highlightWidth = ctrl.showText ? iconReservedWidthPx : btnWidth;
1287 PushRoundedRectangle(ctx, btnX, btnY, highlightWidth, btnHeight, roundedCornerRadiusPx,
1288 0xAA333333, uiRes);
1289 }
1290 }
1291 }
1292 else if (ctrl.type == 3) {
1293 // Future textbox
1294 if (controlVisible) {
1295 PushRoundedRectangle(ctx, btnX, btnY, btnWidth, btnHeight, roundedCornerRadiusPx,
1296 0xFF1E1E1E, uiRes);
1297 }
1298 }
1299 else {
1300 // Plain label
1301 if (controlVisible) {
1302 PushRoundedRectangle(ctx, btnX, btnY, btnWidth, btnHeight, roundedCornerRadiusPx,
1303 0xFF2D2D30, uiRes);
1304 }
1305 }
1306
1307 if (!controlVisible) continue;
1308
1309 float iconX = btnX + (iconReservedWidthPx - iconSizePx) * 0.5f;
1310 float iconY = btnY + (btnHeight - iconSizePx) * 0.5f;
1311 if (!gIconAtlasMetadata.mixedIconCodepoints.empty()) {
1312 const uint32_t randomIconIndex =
1313 ((uint32_t)ctrl.action ^ (uint32_t)i) % (uint32_t)gIconAtlasMetadata.mixedIconCodepoints.size();
1314 PushIcon(ctx, iconX, iconY, iconSizePx, iconSizePx,
1315 gIconAtlasMetadata.mixedIconCodepoints[randomIconIndex], iconColor, uiRes);
1316 }
1317
1318 if (ctrl.showText) {
1319 float textX = btnX + textStartOffsetPx;
1320 float textWidth = btnWidth - textStartOffsetPx - textEndInsetPx;
1321 uint32_t textColor = 0xFFFFFFFF; // default hovered/active color (white)
1322 if (!hovered) {
1323 textColor = ctrl.isEnabled ? uiActiveColors.actionText : 0xAA888888;
1324 }
1325 pushTextClipped(textX, textBaselineY(btnY, btnHeight, uiTextScale),
1326 label, textWidth, textColor, uiTextScale);
1327 }
1328 }
1329
1330 // ACTIVE DROPDOWN (placeholder)
1331 if (window.activeDropdownAction != Commands::INVALID) {
1332 float dropX = 400.0f; // TODO: track real button X for proper positioning
1333 float dropY = topActionGroupY + 80.0f;
1334 pushRect(dropX, dropY, 160, 220, 0xFF1E1E1E);
1335 window.activeDropdownAction = Commands::INVALID; // immediate-mode auto-close
1336 }
1337
1338 // DRAW ALL UI GEOMETRY
1339 if (ctx.indexCount == 0) return;
1340
1341 D3D12_VERTEX_BUFFER_VIEW vbv{};
1342 vbv.BufferLocation = uiRes.uiVertexBuffer->GetGPUVirtualAddress();
1343 vbv.SizeInBytes = ctx.vertexCount * sizeof(UIVertex);
1344 vbv.StrideInBytes = sizeof(UIVertex);
1345
1346 D3D12_INDEX_BUFFER_VIEW ibv{};
1347 ibv.BufferLocation = uiRes.uiIndexBuffer->GetGPUVirtualAddress();
1348 ibv.SizeInBytes = ctx.indexCount * sizeof(uint16_t);
1349 ibv.Format = DXGI_FORMAT_R16_UINT;
1350
1351 cmd->IASetVertexBuffers(0, 1, &vbv);
1352 cmd->IASetIndexBuffer(&ibv);
1353 cmd->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
1354 cmd->DrawIndexedInstanced(ctx.indexCount, 1, 0, 0, 0);
1355}
Miscellaneous philosophy:
Renderer must support these scripts:
| Script | Languages |
|---|---|
| Latin | English, German, French, Spanish, Portuguese, Polish, Dutch, Swedish, Italian |
| Cyrillic | Russian, Ukrainian |
| CJK | Chinese, Japanese |
| Hangul | Korean |
| Arabic | Urdu |
| Indic | Hindi, Bengali, Telugu, Tamil, etc |
| Thai | Thai |
| Vietnamese | Latin + diacritics |
Recommended Font Families: NotoSans-Regular NotoSansCJK-Regular NotoSansDevanagari NotoSansTamil NotoSansTelugu NotoSansThai NotoSansArabic NotoSansHebrew