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