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 constexpr float groupNavWidth = 96.0f;
578 constexpr float groupNavStride = groupNavWidth + 10.0f;
579 for (size_t i = 0; i < TotalTopUIActionGroups; ++i) {
580 layout.actionGroups[i].navX = (float)i * groupNavStride;
581 layout.actionGroups[i].navWidth = groupNavWidth;
582 }
583
584 std::array<bool, TotalTopUIActionGroups> groupSeen{};
585 float currentX = 5.0f;
586 float verticalSlotMaxSize = 0.0f;
587 float contentRight = currentX;
588 int activeSubGroupIndex = -1;
589 size_t activeSubGroupRun = 0;
590
591 for (size_t i = 0; i < TotalUIControls; ++i) {
592 const UIControlDefinition& ctrl = AllUIControls[i];
593 const size_t groupIndex = ctrl.actionGroupIndex;
594 const size_t subGroupIndex = ctrl.actionSubGroupIndex;
595
596 if (groupIndex < TotalTopUIActionGroups && !groupSeen[groupIndex]) {
597 layout.actionGroups[groupIndex].contentStartX = currentX;
598 layout.actionGroups[groupIndex].contentEndX = currentX;
599 groupSeen[groupIndex] = true;
600 }
601
602 if (subGroupIndex < TotalTopUIActionSubGroups &&
603 !layout.actionSubGroups[subGroupIndex].hasControls) {
604 layout.actionSubGroups[subGroupIndex].contentStartX = currentX;
605 layout.actionSubGroups[subGroupIndex].contentEndX = currentX;
606 layout.actionSubGroups[subGroupIndex].hasControls = true;
607 }
608
609 if (activeSubGroupIndex != (int)subGroupIndex &&
610 layout.actionSubGroupRunCount < layout.actionSubGroupRuns.size()) {
611 activeSubGroupIndex = (int)subGroupIndex;
612 activeSubGroupRun = layout.actionSubGroupRunCount++;
613 layout.actionSubGroupRuns[activeSubGroupRun].subGroupIndex = ctrl.actionSubGroupIndex;
614 layout.actionSubGroupRuns[activeSubGroupRun].contentStartX = currentX;
615 layout.actionSubGroupRuns[activeSubGroupRun].contentEndX = currentX;
616 }
617
618 float btnWidth = layout.buttonWidthPx;
619 float btnY = layout.topActionGroupY;
620 const char32_t* label = LocalizedControlLabel(ctrl);
621 if (ctrl.showText && *label != U'\0') {
622 float contentWidth = layout.textStartOffsetPx +
623 MeasureUIStringWidth(label, layout.uiTextScale) + layout.textEndInsetPx;
624 btnWidth = std::max(btnWidth, contentWidth);
625 }
626 if (ctrl.noOfVerticalSlots > 1) {
627 btnY += ctrl.verticalSlotNo * (layout.buttonHeightPx + 1.0f);
628 }
629
630 layout.controls[i] = { currentX, btnY, btnWidth, layout.buttonHeightPx };
631 verticalSlotMaxSize = std::max(verticalSlotMaxSize, btnWidth);
632
633 const bool endOfColumn = (i + 1 == TotalUIControls) || (AllUIControls[i + 1].verticalSlotNo == 0);
634 if (endOfColumn) {
635 const float columnRight = currentX + verticalSlotMaxSize;
636 if (groupIndex < TotalTopUIActionGroups) {
637 layout.actionGroups[groupIndex].contentEndX =
638 std::max(layout.actionGroups[groupIndex].contentEndX, columnRight);
639 }
640 if (subGroupIndex < TotalTopUIActionSubGroups) {
641 layout.actionSubGroups[subGroupIndex].contentEndX =
642 std::max(layout.actionSubGroups[subGroupIndex].contentEndX, columnRight);
643 }
644 if (layout.actionSubGroupRunCount > 0) {
645 layout.actionSubGroupRuns[activeSubGroupRun].contentEndX =
646 std::max(layout.actionSubGroupRuns[activeSubGroupRun].contentEndX, columnRight);
647 }
648 contentRight = std::max(contentRight, columnRight);
649
650 if (i + 1 < TotalUIControls) {
651 currentX = columnRight + layout.buttonGapPx;
652 }
653 verticalSlotMaxSize = 0.0f;
654 }
655 }
656
657 layout.totalContentWidthPx = contentRight + 30.0f;
658 layout.scrollOffsetPx = previousScroll;
659 layout.isValid = true;
660}
661
662// PushRect
663void PushRect( UIDrawContext& ctx, float x, float y, float w, float h,
664 uint32_t color, DX12ResourcesUI& uiRes) {
665 if (ctx.vertexCount + 4 > uiRes.maxVertices) return;
666 if (ctx.indexCount + 6 > uiRes.maxIndices) return;
667
668 uint16_t base = ctx.vertexCount;
669
670 ctx.vertexPtr[0] = { x,y,0,0,color, UI_ENGLISH_ATLAS_SLOT };
671 ctx.vertexPtr[1] = { x + w,y,0,0,color, UI_ENGLISH_ATLAS_SLOT };
672 ctx.vertexPtr[2] = { x + w,y + h,0,0,color, UI_ENGLISH_ATLAS_SLOT };
673 ctx.vertexPtr[3] = { x,y + h,0,0,color, UI_ENGLISH_ATLAS_SLOT };
674
675 ctx.indexPtr[0] = base + 0;
676 ctx.indexPtr[1] = base + 1;
677 ctx.indexPtr[2] = base + 2;
678 ctx.indexPtr[3] = base + 0;
679 ctx.indexPtr[4] = base + 2;
680 ctx.indexPtr[5] = base + 3;
681}
682
683static void PushTexturedQuad(UIDrawContext& ctx, float x, float y, float w, float h,
684 const UIAtlasRegion& region, uint32_t atlasIndex, uint32_t color, DX12ResourcesUI& uiRes) {
685 if (ctx.vertexCount + 4 > uiRes.maxVertices) return;
686 if (ctx.indexCount + 6 > uiRes.maxIndices) return;
687
688 uint16_t base = ctx.vertexCount;
689 ctx.vertexPtr[0] = { x, y, region.uvMinX, region.uvMinY, color, atlasIndex };
690 ctx.vertexPtr[1] = { x + w, y, region.uvMaxX, region.uvMinY, color, atlasIndex };
691 ctx.vertexPtr[2] = { x + w, y + h, region.uvMaxX, region.uvMaxY, color, atlasIndex };
692 ctx.vertexPtr[3] = { x, y + h, region.uvMinX, region.uvMaxY, color, atlasIndex };
693
694 ctx.indexPtr[0] = base + 0;
695 ctx.indexPtr[1] = base + 1;
696 ctx.indexPtr[2] = base + 2;
697 ctx.indexPtr[3] = base + 0;
698 ctx.indexPtr[4] = base + 2;
699 ctx.indexPtr[5] = base + 3;
700
701 ctx.vertexPtr += 4;
702 ctx.indexPtr += 6;
703 ctx.vertexCount += 4;
704 ctx.indexCount += 6;
705}
706
707void PushRoundedRectangle(UIDrawContext& ctx, float x, float y, float w, float h, float radiusPx,
708 uint32_t color, DX12ResourcesUI& uiRes) {
709 if (w <= 0.0f || h <= 0.0f) return;
710 if (ctx.vertexCount + 36 > uiRes.maxVertices) return;
711 if (ctx.indexCount + 54 > uiRes.maxIndices) return;
712
713 const float clampedRadius = std::max(1.0f, std::min(radiusPx, 0.5f * std::min(w, h)));
714 const float xCuts[4] = { x, x + clampedRadius, x + w - clampedRadius, x + w };
715 const float yCuts[4] = { y, y + clampedRadius, y + h - clampedRadius, y + h };
716
717 /*
718 for (int row = 0; row < 3; ++row) {
719 for (int col = 0; col < 3; ++col) {
720 PushTexturedQuad(ctx, xCuts[col], yCuts[row],
721 xCuts[col + 1] - xCuts[col], yCuts[row + 1] - yCuts[row],
722 gIconAtlasMetadata.roundedRectangle.regions[row][col],
723 UI_ICON_ATLAS_SLOT, color, uiRes);
724 }
725 }*/
726 // Unrolled the above loops for better performance (fewer function calls, better instruction-level parallelism)
727 PushTexturedQuad(ctx, xCuts[0], yCuts[0], xCuts[1] - xCuts[0], yCuts[1] - yCuts[0],
728 gIconAtlasMetadata.roundedRectangle.regions[0][0], UI_ICON_ATLAS_SLOT, color, uiRes);
729 PushTexturedQuad(ctx, xCuts[1], yCuts[0], xCuts[2] - xCuts[1], yCuts[1] - yCuts[0],
730 gIconAtlasMetadata.roundedRectangle.regions[0][1], UI_ICON_ATLAS_SLOT, color, uiRes);
731 PushTexturedQuad(ctx, xCuts[2], yCuts[0], xCuts[3] - xCuts[2], yCuts[1] - yCuts[0],
732 gIconAtlasMetadata.roundedRectangle.regions[0][2], UI_ICON_ATLAS_SLOT, color, uiRes);
733 PushTexturedQuad(ctx, xCuts[0], yCuts[1], xCuts[1] - xCuts[0], yCuts[2] - yCuts[1],
734 gIconAtlasMetadata.roundedRectangle.regions[1][0], UI_ICON_ATLAS_SLOT, color, uiRes);
735 PushTexturedQuad(ctx, xCuts[1], yCuts[1], xCuts[2] - xCuts[1], yCuts[2] - yCuts[1],
736 gIconAtlasMetadata.roundedRectangle.regions[1][1], UI_ICON_ATLAS_SLOT, color, uiRes);
737 PushTexturedQuad(ctx, xCuts[2], yCuts[1], xCuts[3] - xCuts[2], yCuts[2] - yCuts[1],
738 gIconAtlasMetadata.roundedRectangle.regions[1][2], UI_ICON_ATLAS_SLOT, color, uiRes);
739 PushTexturedQuad(ctx, xCuts[0], yCuts[2], xCuts[1] - xCuts[0], yCuts[3] - yCuts[2],
740 gIconAtlasMetadata.roundedRectangle.regions[2][0], UI_ICON_ATLAS_SLOT, color, uiRes);
741 PushTexturedQuad(ctx, xCuts[1], yCuts[2], xCuts[2] - xCuts[1], yCuts[3] - yCuts[2],
742 gIconAtlasMetadata.roundedRectangle.regions[2][1], UI_ICON_ATLAS_SLOT, color, uiRes);
743 PushTexturedQuad(ctx, xCuts[2], yCuts[2], xCuts[3] - xCuts[2], yCuts[3] - yCuts[2],
744 gIconAtlasMetadata.roundedRectangle.regions[2][2], UI_ICON_ATLAS_SLOT, color, uiRes);
745}
746
747void PushTopRoundedRectangle(UIDrawContext& ctx, float x, float y, float w, float h, float radiusPx,
748 uint32_t color, DX12ResourcesUI& uiRes) {
749 if (w <= 0.0f || h <= 0.0f) return;
750 if (ctx.vertexCount + 24 > uiRes.maxVertices) return;
751 if (ctx.indexCount + 36 > uiRes.maxIndices) return;
752
753 const float clampedRadius = std::max(1.0f, std::min(radiusPx, 0.5f * std::min(w, h)));
754 const float xCuts[4] = { x, x + clampedRadius, x + w - clampedRadius, x + w };
755 const float yCuts[3] = { y, y + clampedRadius, y + h };
756
757 // Row 0 (Top part with rounded corners)
758 PushTexturedQuad(ctx, xCuts[0], yCuts[0], xCuts[1] - xCuts[0], yCuts[1] - yCuts[0],
759 gIconAtlasMetadata.roundedRectangle.regions[0][0], UI_ICON_ATLAS_SLOT, color, uiRes);
760 PushTexturedQuad(ctx, xCuts[1], yCuts[0], xCuts[2] - xCuts[1], yCuts[1] - yCuts[0],
761 gIconAtlasMetadata.roundedRectangle.regions[0][1], UI_ICON_ATLAS_SLOT, color, uiRes);
762 PushTexturedQuad(ctx, xCuts[2], yCuts[0], xCuts[3] - xCuts[2], yCuts[1] - yCuts[0],
763 gIconAtlasMetadata.roundedRectangle.regions[0][2], UI_ICON_ATLAS_SLOT, color, uiRes);
764
765 // Row 1 (Bottom part with sharp corners utilizing the flat middle row)
766 PushTexturedQuad(ctx, xCuts[0], yCuts[1], xCuts[1] - xCuts[0], yCuts[2] - yCuts[1],
767 gIconAtlasMetadata.roundedRectangle.regions[1][0], UI_ICON_ATLAS_SLOT, color, uiRes);
768 PushTexturedQuad(ctx, xCuts[1], yCuts[1], xCuts[2] - xCuts[1], yCuts[2] - yCuts[1],
769 gIconAtlasMetadata.roundedRectangle.regions[1][1], UI_ICON_ATLAS_SLOT, color, uiRes);
770 PushTexturedQuad(ctx, xCuts[2], yCuts[1], xCuts[3] - xCuts[2], yCuts[2] - yCuts[1],
771 gIconAtlasMetadata.roundedRectangle.regions[1][2], UI_ICON_ATLAS_SLOT, color, uiRes);
772}
773
774static void PushIcon(UIDrawContext& ctx, float x, float y, float w, float h, char32_t iconCodepoint,
775 uint32_t color, DX12ResourcesUI& uiRes) {
776 auto iconIt = iconGlyphLookup.find(iconCodepoint);
777 if (iconIt == iconGlyphLookup.end()) return;
778 const Glyph& glyph = iconIt->second;
779 UIAtlasRegion icon{ glyph.uvMinX, glyph.uvMinY, glyph.uvMaxX, glyph.uvMaxY };
780 PushTexturedQuad(ctx, x, y, w, h, icon, UI_ICON_ATLAS_SLOT, color, uiRes);
781}
782
783// Returns true if clicked this frame
784bool PushInteractiveRect(UIDrawContext& ctx, float x, float y, float w, float h, uint32_t baseColor,
785 uint32_t id, const UIInput& input, DX12ResourcesUI& uiRes, bool enabled = true) {
786 uint32_t color = baseColor;
787
788 bool hovered = enabled && (input.mouseX >= x && input.mouseX < x + w &&
789 input.mouseY >= y && input.mouseY < y + h);
790
791 if (hovered) color = 0xFF555555; // hover tint (TODO: theme-aware)
792 if (hovered && input.leftButtonDown) color = 0xFF333333; // pressed tint
793 if (!enabled) color = 0xFF1E1E1E; // If disabled, force a darker/grayer base color
794
795 PushRect(ctx, x, y, w, h, color, uiRes);
796
797 if (!enabled) return false;// Disabled controls do NOT respond to clicks
798 if (hovered && input.leftButtonPressedThisFrame) {
799 return true;
800 }
801 return false;
802}
803
804void PushText(UIDrawContext& ctx, float x, float y, const char* text, uint32_t color, DX12ResourcesUI& uiRes)
805{
806 float cursorX = x;
807 uint32_t glyphCount = 0;
808
809 for (const char* p = text; *p; ++p)
810 {
811 // Bounds Checking (Crucial for text strings)
812 if (ctx.vertexCount + (glyphCount + 1) * 4 > uiRes.maxVertices) return;
813 if (ctx.indexCount + (glyphCount + 1) * 6 > uiRes.maxIndices) return;
814
815 char c = *p;
816 if (glyphLookup.find(c) == glyphLookup.end()) continue;
817
818 const Glyph& g = glyphLookup[c];
819 if (g.width <= 0 || g.height <= 0) {
820 cursorX += g.advanceX;
821 continue;
822 }
823
824 float xpos = cursorX + g.bearingX;
825 float ypos = y - g.bearingY;
826 float w = (float)g.width;
827 float h = (float)g.height;
828 uint32_t vertexOffset = glyphCount * 4;
829 uint32_t indexOffset = glyphCount * 6;
830
831 // Add 4 vertices. Write relative to the current pointer (0, 1, 2, 3)
832 uint32_t vidx = ctx.vertexCount + vertexOffset;
833 ctx.vertexPtr[vertexOffset + 0] = { xpos, ypos, g.uvMinX, g.uvMinY, color, UI_ENGLISH_ATLAS_SLOT };
834 ctx.vertexPtr[vertexOffset + 1] = { xpos + w, ypos, g.uvMaxX, g.uvMinY, color, UI_ENGLISH_ATLAS_SLOT };
835 ctx.vertexPtr[vertexOffset + 2] = { xpos + w, ypos + h, g.uvMaxX, g.uvMaxY, color, UI_ENGLISH_ATLAS_SLOT };
836 ctx.vertexPtr[vertexOffset + 3] = { xpos, ypos + h, g.uvMinX, g.uvMaxY, color, UI_ENGLISH_ATLAS_SLOT };
837
838 // Add 6 indices. Write indices relative to the current index pointer
839 ctx.indexPtr[indexOffset + 0] = vidx + 0;
840 ctx.indexPtr[indexOffset + 1] = vidx + 1;
841 ctx.indexPtr[indexOffset + 2] = vidx + 2;
842 ctx.indexPtr[indexOffset + 3] = vidx + 0;
843 ctx.indexPtr[indexOffset + 4] = vidx + 2;
844 ctx.indexPtr[indexOffset + 5] = vidx + 3;
845
846 glyphCount++;
847 cursorX += g.advanceX;
848 }
849
850 ctx.vertexPtr += glyphCount * 4;
851 ctx.indexPtr += glyphCount * 6;
852 ctx.vertexCount += glyphCount * 4;
853 ctx.indexCount += glyphCount * 6;
854}
855
856// This function renders the list of tabs, all top menu buttons (with dropdowns if required),
857// side favorite / frequent buttons bars, right side property window, bottom status bar.
858// This is also responsible for all relevant DirectX12 configurations required for rendering User Interface.
859void RenderUIOverlay(SingleUIWindow& window, ID3D12GraphicsCommandList* cmd, DX12ResourcesUI& uiRes,
860 UITopRibbonLayout& topRibbonLayout, float monitorDPIX, float monitorDPIY, const UIInput& input) {
861
862 if (!cmd) return; //Defensive check.
863
864 cmd->SetPipelineState(uiRes.uiPSO.Get());
865 cmd->SetGraphicsRootSignature(uiRes.uiRootSignature.Get());
866
867 // Bind descriptor heap
868 ID3D12DescriptorHeap* heaps[] = { uiRes.srvHeap.Get(), uiRes.samplerHeap.Get() };
869 cmd->SetDescriptorHeaps(_countof(heaps), heaps);
870
871 // Bind the descriptor table (which contains t0 + s0)
872 // Root Parameter 1 = SRV table// must match rootParams[1]
873 cmd->SetGraphicsRootDescriptorTable(1, uiRes.srvHeap->GetGPUDescriptorHandleForHeapStart());
874 // Root Parameter 2 = Sampler table
875 cmd->SetGraphicsRootDescriptorTable(2, uiRes.samplerHeap->GetGPUDescriptorHandleForHeapStart());
876 // Bind ortho constant buffer (still root parameter 0)
877 cmd->SetGraphicsRootConstantBufferView(0, uiRes.uiOrthoConstantBuffer->GetGPUVirtualAddress());
878
879 float W = (float)window.dx.WindowWidth;
880 float H = (float)window.dx.WindowHeight;
881 float* ortho = (float*)uiRes.pOrthoDataBegin;
882
883 ortho[0] = 2 / W; ortho[1] = 0; ortho[2] = 0; ortho[3] = -1;
884 ortho[4] = 0; ortho[5] = -2 / H; ortho[6] = 0; ortho[7] = 1;
885 ortho[8] = 0; ortho[9] = 0; ortho[10] = 1; ortho[11] = 0;
886 ortho[12] = 0; ortho[13] = 0; ortho[14] = 0; ortho[15] = 1;
887
888 cmd->SetGraphicsRootConstantBufferView( 0, uiRes.uiOrthoConstantBuffer->GetGPUVirtualAddress());
889
890 UIDrawContext ctx;
891 ctx.vertexPtr = reinterpret_cast<UIVertex*>(uiRes.pVertexDataBegin);
892 ctx.indexPtr = reinterpret_cast<uint16_t*>(uiRes.pIndexDataBegin);
893 ctx.vertexCount = 0;
894 ctx.indexCount = 0;
895 if (!topRibbonLayout.isValid || topRibbonLayout.dpiX != monitorDPIX || topRibbonLayout.dpiY != monitorDPIY) {
896 PrecomputeTopRibbonLayout(topRibbonLayout, monitorDPIX, monitorDPIY);
897 }
898 if (input.mouseWheelDelta != 0 && input.mouseY >= 0.0f && input.mouseY < topRibbonLayout.topUITotalHeightPx) {
899 const float wheelSteps = input.mouseWheelDelta / (float)WHEEL_DELTA;
900 const float scrollStepPx = std::max(topRibbonLayout.buttonWidthPx * 2.0f, 120.0f);
901 topRibbonLayout.scrollOffsetPx -= wheelSteps * scrollStepPx;
902 }
903 ClampTopRibbonScroll(topRibbonLayout, W);
904
905 float pixelsPerMMx = monitorDPIX / 25.4f;
906 float pixelsPerMMy = monitorDPIY / 25.4f;
907 float iconSizePx = topRibbonLayout.iconSizePx;
908 float buttonHeightPx = topRibbonLayout.buttonHeightPx;
909 float iconReservedWidthPx = topRibbonLayout.iconReservedWidthPx;
910 float textStartOffsetPx = topRibbonLayout.textStartOffsetPx;
911 float textEndInsetPx = topRibbonLayout.textEndInsetPx;
912 float tabBarHeightPx = topRibbonLayout.tabBarHeightPx;
913 float topUITotalHeightPx = topRibbonLayout.topUITotalHeightPx;
914 float roundedCornerRadiusPx = topRibbonLayout.roundedCornerRadiusPx;
915
916 auto canPushRect = [&]() {
917 return ctx.vertexCount + 4 <= uiRes.maxVertices &&
918 ctx.indexCount + 6 <= uiRes.maxIndices;
919 };
920
921 auto incrementVertexIndexCounters = [&]() {
922 ctx.vertexPtr += 4;
923 ctx.indexPtr += 6;
924 ctx.vertexCount += 4;
925 ctx.indexCount += 6;
926 };
927
928 auto pushRect = [&](float x, float y, float w, float h, uint32_t color) {
929 bool pushed = canPushRect();
930 PushRect(ctx, x, y, w, h, color, uiRes);
931 if (pushed) incrementVertexIndexCounters();
932 };
933
934 const float uiTextScale = topRibbonLayout.uiTextScale;
935
936 auto pushTextClipped = [&](float x, float y, const char32_t* text, float maxWidth, uint32_t color,
937 float scale) {
938 if (!text || maxWidth <= 0.0f) return;
939
940 float cursorX = x;
941 float textRight = x + maxWidth;
942
943 for (const char32_t* p = text; *p; ++p) {
944 if (*p > 0x7F) continue;
945
946 auto glyphIt = glyphLookup.find(*p);
947 if (glyphIt == glyphLookup.end()) continue;
948
949 const Glyph& g = glyphIt->second;
950 if (g.width <= 0 || g.height <= 0) {
951 cursorX += (float)g.advanceX * scale;
952 continue;
953 }
954
955 // It is always better to be aligned to pixels for better text clarity.
956 float xpos = std::floor(cursorX + (float)g.bearingX * scale + 0.5f);
957 float ypos = std::floor(y - (float)g.bearingY * scale + 0.5f);
958 float glyphWidth = (float)g.width * scale;
959 float glyphHeight = (float)g.height * scale;
960 float glyphRight = xpos + glyphWidth;
961
962 if (glyphRight > textRight) break;
963 if (ctx.vertexCount + 4 > uiRes.maxVertices) return;
964 if (ctx.indexCount + 6 > uiRes.maxIndices) return;
965
966 uint16_t base = ctx.vertexCount;
967 ctx.vertexPtr[0] = { xpos, ypos, g.uvMinX, g.uvMinY, color, UI_ENGLISH_ATLAS_SLOT };
968 ctx.vertexPtr[1] = { xpos + glyphWidth, ypos, g.uvMaxX, g.uvMinY, color, UI_ENGLISH_ATLAS_SLOT };
969 ctx.vertexPtr[2] = { xpos + glyphWidth, ypos + glyphHeight, g.uvMaxX, g.uvMaxY, color, UI_ENGLISH_ATLAS_SLOT };
970 ctx.vertexPtr[3] = { xpos, ypos + glyphHeight, g.uvMinX, g.uvMaxY, color, UI_ENGLISH_ATLAS_SLOT };
971
972 ctx.indexPtr[0] = base + 0;
973 ctx.indexPtr[1] = base + 1;
974 ctx.indexPtr[2] = base + 2;
975 ctx.indexPtr[3] = base + 0;
976 ctx.indexPtr[4] = base + 2;
977 ctx.indexPtr[5] = base + 3;
978
979 ctx.vertexPtr += 4;
980 ctx.indexPtr += 6;
981 ctx.vertexCount += 4;
982 ctx.indexCount += 6;
983 cursorX += (float)g.advanceX * scale;
984 }
985 };
986
987 auto textBaselineY = [&](float y, float h, float scale) {
988 auto glyphIt = glyphLookup.find(U'M');
989 if (glyphIt == glyphLookup.end()) return y + h * 0.7f;
990
991 const Glyph& g = glyphIt->second;
992 return y + h * 0.5f + (float)g.bearingY * scale - (float)g.height * scale * 0.5f;
993 };
994
995 // ENGINEERING / PROJECT TABs
996 // Action ids for engineering thread control (UI -> engineering)
997 constexpr uint32_t ACTION_ENGINEERING_CLOSE = 0xE0000001u;
998 constexpr uint32_t ACTION_ENGINEERING_CREATE = 0xE0000002u;
999
1000 float currentX = 0.0f;
1001
1002 uint16_t tabCount = publishedTabCount.load(std::memory_order_acquire);
1003 uint16_t* tabList = publishedTabIndexes.load(std::memory_order_acquire);
1004
1005 if (canPushRect()) {
1006 PushRect(ctx, 0.0f, 0.0f, 5000.0f, topUITotalHeightPx, uiActiveColors.actionGroupBackground, uiRes);//
1007 incrementVertexIndexCounters();
1008 }
1009
1010 if (canPushRect()) {
1011 PushRect(ctx, 0.0f, 0.0f, 5000.0f, tabBarHeightPx, uiActiveColors.tabBackground, uiRes);//
1012 incrementVertexIndexCounters();
1013 }
1014 // We will allow tabs to shrink progressively when too many tabs exist.
1015 // Compute sizing constraints
1016 const float defaultTabWidth = 160.0f; // legacy fixed width in pixels
1017 const float plusButtonWidth = buttonHeightPx; // reserve square area for '+'
1018 const float minTabWidth = std::max(4.0f * pixelsPerMMx, 8.0f); // 4mm minimum as requested, but at least 8px
1019
1020 // Determine how many slots we need to fit: tabs + one slot for '+' button
1021 uint16_t slotsNeeded = tabCount + 1;
1022 float availableForTabs = std::max(0.0f, W - plusButtonWidth);
1023
1024 float tentativeWidth = availableForTabs / (float)slotsNeeded;
1025 float tabWidthPx = defaultTabWidth;
1026 uint16_t visibleTabs = tabCount;
1027
1028 if (tentativeWidth >= defaultTabWidth) {
1029 tabWidthPx = defaultTabWidth;
1030 } else if (tentativeWidth >= minTabWidth) {
1031 tabWidthPx = tentativeWidth;
1032 } else {
1033 // If tentative width is below minimum, we must hide some tabs.
1034 visibleTabs = (uint16_t)std::floor(availableForTabs / minTabWidth);
1035 if (visibleTabs > tabCount) visibleTabs = tabCount;
1036 tabWidthPx = minTabWidth;
1037 }
1038
1039 // Render visible tabs only; hidden tabs are not drawn (will be handled by horizontal scroll in future)
1040 // Gap between tabs: 0.5 mm on either side
1041 float gapPx = 0.5f * pixelsPerMMx;
1042 for (uint16_t i = 0; i < visibleTabs; i++) {
1043 uint16_t tabID = tabList[i];
1044 bool isActive = (window.activeTabIndex == tabID);
1045
1046 // area for this tab (slot)
1047 float tabX = currentX;
1048 float tabW = tabWidthPx;
1049
1050 // content area inset by half-mm gaps on either side
1051 float contentX = tabX + gapPx;
1052 float contentW = std::max(0.0f, tabW - 2.0f * gapPx);
1053
1054 // X (close) button sizing โ square inside tab on the right
1055 float xBtnSize = std::round(std::min(tabW * 0.5f, std::max( (float)std::round(UI_ICON_SIZE_MM * pixelsPerMMx), 10.0f)));
1056 if (xBtnSize + 4.0f > tabW) xBtnSize = std::max(4.0f, tabW - 4.0f);
1057 float xBtnX = tabX + tabW - xBtnSize - 4.0f;
1058 float xBtnY = std::floor((tabBarHeightPx - xBtnSize) * 0.5f);
1059
1060 // Entire tab background โ draw only inside content area leaving gaps between tabs
1061 if (isActive) {
1062 PushTopRoundedRectangle(ctx, contentX, 0.0f, contentW, tabBarHeightPx, roundedCornerRadiusPx, uiActiveColors.actionGroupBackground, uiRes);
1063 } else {
1064 bool pushed = canPushRect();
1065 PushRect(ctx, contentX, 0.0f, contentW, tabBarHeightPx, uiActiveColors.tabBackground, uiRes);
1066 if (pushed) incrementVertexIndexCounters();
1067 }
1068
1069 // Check clicks on the X button first to avoid activating the tab when user intends to close
1070 bool xHovered = input.mouseX >= xBtnX && input.mouseX < xBtnX + xBtnSize &&
1071 input.mouseY >= xBtnY && input.mouseY < xBtnY + xBtnSize;
1072 if (xHovered && input.leftButtonPressedThisFrame) {
1073 // Signal close intent to engineering thread. Pass tabID in parameter p1.
1074 PushUIAction(ACTION_ENGINEERING_CLOSE, (uint32_t)tabID, 0);
1075 }
1076
1077 // If user clicked on non-X area of tab, activate it
1078 bool tabHovered = input.mouseX >= tabX && input.mouseX < tabX + tabW &&
1079 input.mouseY >= 0 && input.mouseY < tabBarHeightPx;
1080 if (!xHovered && tabHovered && input.leftButtonPressedThisFrame) {
1081 window.activeTabIndex = tabID; // Render thread will draw this tab's geometry on the next frame.
1082 }
1083
1084 // Draw the X button: only draw rounded background when hovered, otherwise render as plain text
1085 char32_t xChar[2] = { U'x', U'\0' };
1086 if (xHovered) {
1087 PushRoundedRectangle(ctx, xBtnX, xBtnY, xBtnSize, xBtnSize, std::max(1.0f, roundedCornerRadiusPx * 0.6f),
1088 0xFF444444, uiRes);
1089 pushTextClipped(xBtnX + 2.0f, textBaselineY(xBtnY, xBtnSize, uiTextScale), xChar, xBtnSize - 4.0f, 0xFFFFFFFF, uiTextScale);
1090 } else {
1091 // Render as plain small text matching tab text color
1092 pushTextClipped(xBtnX + 2.0f, textBaselineY(xBtnY, xBtnSize, uiTextScale), xChar, xBtnSize - 4.0f, uiActiveColors.tabBackgroundText, uiTextScale);
1093 }
1094
1095 // Draw label clipped to remaining area (avoid overlapping with X)
1096 std::u32string tabLabel;
1097 tabLabel.reserve(allTabs[tabID].fileName.size());
1098 for (wchar_t ch : allTabs[tabID].fileName) {
1099 if (ch <= 0x7F) tabLabel.push_back(static_cast<char32_t>(ch));
1100 }
1101
1102 float labelMaxWidth = contentW - (8.0f + xBtnSize + 4.0f);
1103 pushTextClipped(contentX + 8.0f, textBaselineY(0.0f, tabBarHeightPx, uiTextScale), tabLabel.c_str(), labelMaxWidth, uiActiveColors.tabBackgroundText, uiTextScale);
1104
1105 currentX += tabW;
1106
1107 // Draw 1px vertical separator centered in the gap between tabs (only between tabs)
1108 if (i + 1 < visibleTabs) {
1109 float sepX = tabX + tabW; // center of gap between this tab and next
1110 // align to pixel for crispness
1111 float sepXi = std::floor(sepX + 0.5f);
1112 pushRect(sepXi, 2.0f, 1.0f, tabBarHeightPx - 4.0f, uiActiveColors.actionGroupSeperator);
1113 }
1114 }
1115
1116 // If some tabs are hidden, we may draw a subtle indicator (ellipsis) โ skip for now
1117
1118 // Render '+' create new thread button at the end of tab bar
1119 float plusX = currentX + 6.0f; // small padding before plus
1120 float plusSize = std::max(plusButtonWidth, std::round(UI_ICON_SIZE_MM * pixelsPerMMy) + 8.0f);
1121 bool plusHovered = input.mouseX >= plusX && input.mouseX < plusX + plusSize &&
1122 input.mouseY >= (tabBarHeightPx - plusSize) * 0.5f && input.mouseY < (tabBarHeightPx - plusSize) * 0.5f + plusSize;
1123 // '+' button: show rounded background only on hover; otherwise render as plain icon/text
1124 if (plusHovered) {
1125 PushRoundedRectangle(ctx, plusX, (tabBarHeightPx - plusSize) * 0.5f, plusSize, plusSize, roundedCornerRadiusPx,
1126 0xFF444444, uiRes);
1127 if (!gIconAtlasMetadata.mixedIconCodepoints.empty()) {
1128 PushIcon(ctx, plusX + (plusSize - iconSizePx) * 0.5f, (tabBarHeightPx - iconSizePx) * 0.5f,
1129 iconSizePx, iconSizePx, gIconAtlasMetadata.mixedIconCodepoints[0], 0xFFFFFFFF, uiRes);
1130 }
1131 } else {
1132 if (!gIconAtlasMetadata.mixedIconCodepoints.empty()) {
1133 PushIcon(ctx, plusX + (plusSize - iconSizePx) * 0.5f, (tabBarHeightPx - iconSizePx) * 0.5f,
1134 iconSizePx, iconSizePx, gIconAtlasMetadata.mixedIconCodepoints[0], uiActiveColors.tabBackgroundText, uiRes);
1135 }
1136 }
1137 if (plusHovered && input.leftButtonPressedThisFrame) {
1138 PushUIAction(ACTION_ENGINEERING_CREATE, 0, 0);
1139 }
1140
1141 // TOP BUTTONS (ACTION GROUP BAR)
1142 const float buttonGap = topRibbonLayout.buttonGapPx;
1143 const float actionGroupLabelY = topRibbonLayout.actionGroupLabelY;
1144 const float groupLabelHeight = topRibbonLayout.actionGroupLabelHeightPx;
1145 const float topActionGroupY = topRibbonLayout.topActionGroupY;
1146 const float actionSubGroupLabelY = topRibbonLayout.actionSubGroupLabelY;
1147 const float ribbonScrollX = topRibbonLayout.scrollOffsetPx;
1148
1149 // Draw the 5-pixel high extent-of-ribbon-visible visualization bar in the 5px gap below Action Group labels.
1150 // The gap starts at topActionGroupY - 5.0f.
1151 float extentX = 0.0f;
1152 float extentW = W;
1153 if (topRibbonLayout.totalContentWidthPx > W) {
1154 extentX = (topRibbonLayout.scrollOffsetPx / topRibbonLayout.totalContentWidthPx) * W;
1155 extentW = (W / topRibbonLayout.totalContentWidthPx) * W;
1156 }
1157 // Draw active indicator (orange)
1158 pushRect(extentX, topActionGroupY - 5.0f, extentW, 5.0f, 0xFF3399FF);
1159
1160 for (size_t groupIndex = 0; groupIndex < TotalTopUIActionGroups; ++groupIndex) {
1161 const UIActionGroupNames& group = topUIActionGroupNames[groupIndex];
1162 const UITopRibbonActionGroupLayout& groupLayout = topRibbonLayout.actionGroups[groupIndex];
1163 const char32_t* label = LocalizedUIString(group.labelStringID);
1164 const bool hovered = group.isEnabled &&
1165 input.mouseX >= groupLayout.navX && input.mouseX < groupLayout.navX + groupLayout.navWidth &&
1166 input.mouseY >= actionGroupLabelY && input.mouseY < actionGroupLabelY + groupLabelHeight;
1167
1168 if (hovered) {
1169 pushRect(groupLayout.navX, actionGroupLabelY, groupLayout.navWidth, groupLabelHeight,
1170 uiActiveColors.tabBackgroundHover);
1171 }
1172 if (hovered && input.leftButtonPressedThisFrame) {
1173 topRibbonLayout.scrollOffsetPx = groupLayout.contentStartX;
1174 ClampTopRibbonScroll(topRibbonLayout, W);
1175 }
1176
1177 pushTextClipped(groupLayout.navX + 4.0f, textBaselineY(actionGroupLabelY, groupLabelHeight, uiTextScale),
1178 label, groupLayout.navWidth - 8.0f, uiActiveColors.actionText, uiTextScale);
1179 }
1180
1181 for (size_t runIndex = 0; runIndex < topRibbonLayout.actionSubGroupRunCount; ++runIndex) {
1182 const UITopRibbonSubGroupRunLayout& run = topRibbonLayout.actionSubGroupRuns[runIndex];
1183 if (run.subGroupIndex >= TotalTopUIActionSubGroups) continue;
1184
1185 const UIActionGroupNames& subGroup = topUIActionSubGroupNames[run.subGroupIndex];
1186 const char32_t* label = LocalizedUIString(subGroup.labelStringID);
1187 const float runX = run.contentStartX - ribbonScrollX;
1188 const float runWidth = std::max(0.0f, run.contentEndX - run.contentStartX);
1189 const float labelWidth = MeasureUIStringWidth(label, uiTextScale);
1190 const float labelX = runX + std::max(4.0f, (runWidth - labelWidth) * 0.5f);
1191
1192 pushTextClipped(labelX, textBaselineY(actionSubGroupLabelY, groupLabelHeight, uiTextScale),
1193 label, std::max(0.0f, runWidth - 8.0f), uiActiveColors.actionText, uiTextScale);
1194
1195 if (runIndex + 1 < topRibbonLayout.actionSubGroupRunCount) {
1196 const float lineX = std::floor(run.contentEndX + buttonGap * 0.5f - ribbonScrollX);
1197 const float lineHeight = 3.0f * topRibbonLayout.buttonHeightPx + 2.0f;
1198 if (lineX >= -1.0f && lineX <= W + 1.0f) {
1199 pushRect(lineX, topActionGroupY, 1.0f, lineHeight, 0xFF555555);
1200 }
1201 }
1202 }
1203
1204 for (size_t i = 0; i < TotalUIControls; ++i) {
1205 const auto& ctrl = AllUIControls[i];
1206 const UITopRibbonControlLayout& ctrlLayout = topRibbonLayout.controls[i];
1207 const float btnX = ctrlLayout.x - ribbonScrollX;
1208 const float btnY = ctrlLayout.y;
1209 const float btnWidth = ctrlLayout.width;
1210 const float btnHeight = ctrlLayout.height;
1211 const char32_t* label = LocalizedControlLabel(ctrl);
1212 uint32_t baseColor = StableRandomUIColour((uint32_t)ctrl.action ^ ((uint32_t)i * 0x9E3779B9u));// Render
1213 uint32_t iconColor = StableRandomUIColour(((uint32_t)ctrl.action << 1) ^ 0xA511E9B3u ^ (uint32_t)i);
1214 const bool controlVisible = btnX + btnWidth >= 0.0f && btnX <= W;
1215 bool hovered = false;
1216
1217 if (ctrl.type == 1 || ctrl.type == 2) { // Button or Dropdown trigger
1218 hovered = controlVisible && ctrl.isEnabled && (input.mouseX >= btnX && input.mouseX < btnX + btnWidth &&
1219 input.mouseY >= btnY && input.mouseY < btnY + btnHeight);
1220 uint32_t drawColor = hovered && input.leftButtonDown ? 0xFF333333 : baseColor;
1221 if (hovered && !input.leftButtonDown) drawColor = 0xFF555555;
1222 if (controlVisible) {
1223 if (hovered) {
1224 PushRoundedRectangle(ctx, btnX, btnY, btnWidth, btnHeight, roundedCornerRadiusPx,
1225 drawColor, uiRes);
1226 } else {
1227 float highlightWidth = ctrl.showText ? iconReservedWidthPx : btnWidth;
1228 PushRoundedRectangle(ctx, btnX, btnY, highlightWidth, btnHeight, roundedCornerRadiusPx,
1229 baseColor, uiRes);
1230 }
1231 }
1232
1233 bool clicked = hovered && input.leftButtonPressedThisFrame;
1234
1235 if (clicked && ctrl.isEnabled) {
1236 PushUIAction((uint32_t)ctrl.action);
1237 if (ctrl.zIndex == 1) { // Dropdown trigger
1238 window.activeDropdownAction = ctrl.action;
1239 }
1240 }
1241
1242 if (!ctrl.isEnabled) { // Gray-out overlay for disabled controls
1243 if (controlVisible) {
1244 float highlightWidth = ctrl.showText ? iconReservedWidthPx : btnWidth;
1245 PushRoundedRectangle(ctx, btnX, btnY, highlightWidth, btnHeight, roundedCornerRadiusPx,
1246 0xAA333333, uiRes);
1247 }
1248 }
1249 }
1250 else if (ctrl.type == 3) {
1251 // Future textbox
1252 if (controlVisible) {
1253 PushRoundedRectangle(ctx, btnX, btnY, btnWidth, btnHeight, roundedCornerRadiusPx,
1254 0xFF1E1E1E, uiRes);
1255 }
1256 }
1257 else {
1258 // Plain label
1259 if (controlVisible) {
1260 PushRoundedRectangle(ctx, btnX, btnY, btnWidth, btnHeight, roundedCornerRadiusPx,
1261 0xFF2D2D30, uiRes);
1262 }
1263 }
1264
1265 if (!controlVisible) continue;
1266
1267 float iconX = btnX + (iconReservedWidthPx - iconSizePx) * 0.5f;
1268 float iconY = btnY + (btnHeight - iconSizePx) * 0.5f;
1269 if (!gIconAtlasMetadata.mixedIconCodepoints.empty()) {
1270 const uint32_t randomIconIndex =
1271 ((uint32_t)ctrl.action ^ (uint32_t)i) % (uint32_t)gIconAtlasMetadata.mixedIconCodepoints.size();
1272 PushIcon(ctx, iconX, iconY, iconSizePx, iconSizePx,
1273 gIconAtlasMetadata.mixedIconCodepoints[randomIconIndex], iconColor, uiRes);
1274 }
1275
1276 if (ctrl.showText) {
1277 float textX = btnX + textStartOffsetPx;
1278 float textWidth = btnWidth - textStartOffsetPx - textEndInsetPx;
1279 uint32_t textColor = 0xFFFFFFFF; // default hovered/active color (white)
1280 if (!hovered) {
1281 textColor = ctrl.isEnabled ? uiActiveColors.actionText : 0xAA888888;
1282 }
1283 pushTextClipped(textX, textBaselineY(btnY, btnHeight, uiTextScale),
1284 label, textWidth, textColor, uiTextScale);
1285 }
1286 }
1287
1288 // ACTIVE DROPDOWN (placeholder)
1289 if (window.activeDropdownAction != Commands::INVALID) {
1290 float dropX = 400.0f; // TODO: track real button X for proper positioning
1291 float dropY = topActionGroupY + 80.0f;
1292 pushRect(dropX, dropY, 160, 220, 0xFF1E1E1E);
1293 window.activeDropdownAction = Commands::INVALID; // immediate-mode auto-close
1294 }
1295
1296 // DRAW ALL UI GEOMETRY
1297 if (ctx.indexCount == 0) return;
1298
1299 D3D12_VERTEX_BUFFER_VIEW vbv{};
1300 vbv.BufferLocation = uiRes.uiVertexBuffer->GetGPUVirtualAddress();
1301 vbv.SizeInBytes = ctx.vertexCount * sizeof(UIVertex);
1302 vbv.StrideInBytes = sizeof(UIVertex);
1303
1304 D3D12_INDEX_BUFFER_VIEW ibv{};
1305 ibv.BufferLocation = uiRes.uiIndexBuffer->GetGPUVirtualAddress();
1306 ibv.SizeInBytes = ctx.indexCount * sizeof(uint16_t);
1307 ibv.Format = DXGI_FORMAT_R16_UINT;
1308
1309 cmd->IASetVertexBuffers(0, 1, &vbv);
1310 cmd->IASetIndexBuffer(&ibv);
1311 cmd->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
1312 cmd->DrawIndexedInstanced(ctx.indexCount, 1, 0, 0, 0);
1313}
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