xbintsc 0.3.46 → 0.3.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/AGENTS.md +95 -0
  2. package/README.md +25 -0
  3. package/README.zh-CN.md +23 -0
  4. package/dist/src/cli/hints.d.ts +54 -0
  5. package/dist/src/cli/hints.js +165 -0
  6. package/dist/src/cli/hints.js.map +1 -0
  7. package/dist/src/cli/main.js +73 -9
  8. package/dist/src/cli/main.js.map +1 -1
  9. package/dist/src/codegen/generator/tables.d.ts +26 -0
  10. package/dist/src/codegen/generator/tables.js +64 -12
  11. package/dist/src/codegen/generator/tables.js.map +1 -1
  12. package/dist/src/diagnostics/source-text.d.ts +22 -0
  13. package/dist/src/diagnostics/source-text.js +76 -0
  14. package/dist/src/diagnostics/source-text.js.map +1 -0
  15. package/dist/src/driver/bundler/graph.js +2 -1
  16. package/dist/src/driver/bundler/graph.js.map +1 -1
  17. package/dist/src/driver/compiler.js +28 -3
  18. package/dist/src/driver/compiler.js.map +1 -1
  19. package/dist/src/driver/config.d.ts +7 -0
  20. package/dist/src/driver/config.js.map +1 -1
  21. package/dist/src/driver/win-entry.d.ts +21 -0
  22. package/dist/src/driver/win-entry.js +52 -0
  23. package/dist/src/driver/win-entry.js.map +1 -0
  24. package/dist/src/lexer/scanner/strings.js +16 -3
  25. package/dist/src/lexer/scanner/strings.js.map +1 -1
  26. package/dist/tests/cli/hints.test.d.ts +9 -0
  27. package/dist/tests/cli/hints.test.js +143 -0
  28. package/dist/tests/cli/hints.test.js.map +1 -0
  29. package/dist/tests/cli/main.test.js +6 -4
  30. package/dist/tests/cli/main.test.js.map +1 -1
  31. package/dist/tests/codegen/llvm.test.js +17 -2
  32. package/dist/tests/codegen/llvm.test.js.map +1 -1
  33. package/dist/tests/e2e/gui-example-browser.test.d.ts +12 -0
  34. package/dist/tests/e2e/gui-example-browser.test.js +164 -0
  35. package/dist/tests/e2e/gui-example-browser.test.js.map +1 -0
  36. package/dist/tests/e2e/gui-example.test.d.ts +14 -0
  37. package/dist/tests/e2e/gui-example.test.js +76 -0
  38. package/dist/tests/e2e/gui-example.test.js.map +1 -0
  39. package/dist/tests/e2e/gui-helpers.d.ts +39 -4
  40. package/dist/tests/e2e/gui-helpers.js +38 -9
  41. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  42. package/dist/tests/e2e/gui-layout.test.js +155 -0
  43. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  44. package/dist/tests/e2e/gui-lifecycle.test.js +10 -1
  45. package/dist/tests/e2e/gui-lifecycle.test.js.map +1 -1
  46. package/dist/tests/e2e/gui-paint.test.js +44 -4
  47. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  48. package/dist/tests/e2e/gui-script.test.js +8 -4
  49. package/dist/tests/e2e/gui-script.test.js.map +1 -1
  50. package/dist/tests/helpers.js +3 -2
  51. package/dist/tests/helpers.js.map +1 -1
  52. package/dist/tests/lexer/strings.test.js +14 -2
  53. package/dist/tests/lexer/strings.test.js.map +1 -1
  54. package/doc/DESIGN.md +117 -0
  55. package/doc/ai/README.md +63 -0
  56. package/doc/ai/build-recipe.md +137 -0
  57. package/doc/ai/cli.md +142 -0
  58. package/doc/ai/contributing.md +196 -0
  59. package/doc/ai/extensions.md +148 -0
  60. package/doc/ai/language-support.md +152 -0
  61. package/doc/ai/troubleshooting.md +163 -0
  62. package/doc/ai/zh-CN/README.md +56 -0
  63. package/doc/ai/zh-CN/build-recipe.md +132 -0
  64. package/doc/ai/zh-CN/cli.md +127 -0
  65. package/doc/ai/zh-CN/contributing.md +173 -0
  66. package/doc/ai/zh-CN/extensions.md +139 -0
  67. package/doc/ai/zh-CN/language-support.md +147 -0
  68. package/doc/ai/zh-CN/troubleshooting.md +150 -0
  69. package/doc/gui-scripts.md +350 -0
  70. package/doc/gui.md +752 -0
  71. package/doc/icon.md +265 -0
  72. package/doc/implemented.md +373 -0
  73. package/doc/node-implemented.md +588 -0
  74. package/doc/node-unimplemented.md +167 -0
  75. package/doc/post/announce.md +43 -0
  76. package/doc/requirements.md +145 -0
  77. package/doc/unimplemented.md +286 -0
  78. package/doc/xbintsc.config.schema.json +71 -0
  79. package/doc/zh-CN/DESIGN.md +104 -0
  80. package/doc/zh-CN/gui-scripts.md +329 -0
  81. package/doc/zh-CN/gui.md +656 -0
  82. package/doc/zh-CN/icon.md +241 -0
  83. package/doc/zh-CN/implemented.md +365 -0
  84. package/doc/zh-CN/node-implemented.md +533 -0
  85. package/doc/zh-CN/node-unimplemented.md +141 -0
  86. package/doc/zh-CN/plan-require-node-modules.md +284 -0
  87. package/doc/zh-CN/post/announce.md +47 -0
  88. package/doc/zh-CN/requirements.md +134 -0
  89. package/doc/zh-CN/unimplemented.md +247 -0
  90. package/llms.txt +45 -0
  91. package/package.json +4 -1
  92. package/runtime/ext_gui/dom_api.cpp +19 -0
  93. package/runtime/ext_gui/dom_api_node.cpp +4 -1
  94. package/runtime/ext_gui/dom_api_node_read.cpp +12 -0
  95. package/runtime/ext_gui/gui.cpp +45 -3
  96. package/runtime/ext_gui/gui_engine.h +15 -0
  97. package/runtime/ext_gui/gui_events.cpp +10 -3
  98. package/runtime/ext_gui/layout.cpp +6 -0
  99. package/runtime/ext_gui/layout.h +4 -0
  100. package/runtime/ext_gui/layout_flex.cpp +13 -5
  101. package/runtime/ext_gui/layout_flow.cpp +134 -4
  102. package/runtime/ext_gui/layout_flow_internal.h +41 -2
  103. package/runtime/ext_gui/layout_inline.cpp +11 -3
  104. package/runtime/ext_gui/paint.cpp +6 -0
  105. package/runtime/ext_gui/renderer.cpp +13 -11
  106. package/runtime/ext_gui/renderer_image.cpp +12 -8
  107. package/runtime/ext_gui/renderer_shaders.h +131 -4
  108. package/runtime/ext_gui/renderer_shaders_data.h +1809 -0
  109. package/runtime/ext_gui/renderer_text.cpp +12 -8
  110. package/runtime/ext_gui/script.cpp +12 -2
  111. package/runtime/ext_gui/shaders.hlsl +98 -0
  112. package/runtime/ext_gui/spirv/fill.frag +19 -0
  113. package/runtime/ext_gui/spirv/fill.vert +42 -0
  114. package/runtime/ext_gui/spirv/image.frag +16 -0
  115. package/runtime/ext_gui/spirv/quad.vert +30 -0
  116. package/runtime/ext_gui/spirv/text.frag +16 -0
  117. package/runtime/ext_gui/text.cpp +14 -0
  118. package/runtime/ext_gui/window.cpp +12 -0
  119. package/runtime/rt.h +10 -0
  120. package/scripts/build-gui-shaders.mjs +204 -0
  121. package/scripts/build-gui.ts +35 -0
  122. package/scripts/check-file-length.ts +5 -1
  123. package/src/cli/hints.ts +194 -0
  124. package/src/cli/main.ts +82 -9
  125. package/src/codegen/generator/tables.ts +60 -14
  126. package/src/diagnostics/source-text.ts +78 -0
  127. package/src/driver/bundler/graph.ts +2 -1
  128. package/src/driver/compiler.ts +29 -3
  129. package/src/driver/config.ts +7 -0
  130. package/src/driver/win-entry.ts +59 -0
  131. package/src/lexer/scanner/strings.ts +16 -3
@@ -48,6 +48,8 @@ float Layouter::preferredContentWidth(const LayoutBox *box, float available) con
48
48
  float best = 0;
49
49
  for (const std::unique_ptr<LayoutBox> &child : box->children) {
50
50
  if (child->display == Display::None) continue;
51
+ /* Out-of-flow children do not contribute to the parent's intrinsic width. */
52
+ if (isAbsoluteChild(child.get())) continue;
51
53
  if (child->is_text) {
52
54
  best = std::max(best, textWidth(child->text, *child->style));
53
55
  } else {
@@ -91,18 +93,107 @@ void Layouter::resolveEdges(LayoutBox *box, float containingWidth) const {
91
93
  }
92
94
  }
93
95
 
96
+ bool Layouter::establishesContainingBlock(const LayoutBox *box) {
97
+ return box->style != nullptr && box->style->position != Position::Static;
98
+ }
99
+
100
+ const LayoutBox *Layouter::containingBlockAncestor(const LayoutBox *box) const {
101
+ /* Absolute boxes resolve against the padding box of the nearest ancestor with
102
+ * `position != static`; when there is none, the caller falls back to the
103
+ * initial containing block (the viewport). */
104
+ for (const LayoutBox *ancestor = box; ancestor != nullptr; ancestor = ancestor->parent) {
105
+ if (establishesContainingBlock(ancestor)) return ancestor;
106
+ }
107
+ return nullptr;
108
+ }
109
+
110
+ void Layouter::setContainingBlock(LayoutBox *box, const LayoutBox *ancestor) const {
111
+ if (ancestor != nullptr) {
112
+ /* The padding box of the ancestor. Its height is only known once the
113
+ * ancestor's own layout has finished, which is why `publishContainingBlock`
114
+ * is called again at the end of `layoutBlock` for the box that owns it. */
115
+ box->cb_x = ancestor->content_x - ancestor->padding_left;
116
+ box->cb_y = ancestor->content_y - ancestor->padding_top;
117
+ box->cb_width = ancestor->padding_left + ancestor->content_width + ancestor->padding_right;
118
+ box->cb_height = ancestor->padding_top + ancestor->content_height + ancestor->padding_bottom;
119
+ return;
120
+ }
121
+ /* No positioned ancestor: the initial containing block is the viewport. */
122
+ box->cb_x = 0;
123
+ box->cb_y = 0;
124
+ box->cb_width = vw;
125
+ box->cb_height = vh;
126
+ }
127
+
128
+ void Layouter::publishContainingBlock(LayoutBox *box) const {
129
+ if (!establishesContainingBlock(box)) return;
130
+ for (const std::unique_ptr<LayoutBox> &child : box->children) setContainingBlock(child.get(), box);
131
+ }
132
+
133
+ void Layouter::applyPosition(LayoutBox *box) const {
134
+ if (box->style == nullptr) return;
135
+ const XtStyle &style = *box->style;
136
+ if (style.position == Position::Static) return;
137
+ float em = style.font_size;
138
+
139
+ if (style.position == Position::Relative) {
140
+ /* Relative positioning offsets the box (and its subtree) without changing
141
+ * the space it occupies in the flow. */
142
+ float dx = 0;
143
+ float dy = 0;
144
+ if (!isAuto(style.left)) dx = px(style.left, box->cb_width, em);
145
+ else if (!isAuto(style.right)) dx = -px(style.right, box->cb_width, em);
146
+ if (!isAuto(style.top)) dy = px(style.top, box->cb_height, em);
147
+ else if (!isAuto(style.bottom)) dy = -px(style.bottom, box->cb_height, em);
148
+ if (dx != 0 || dy != 0) translate(box, dx, dy);
149
+ return;
150
+ }
151
+
152
+ /* Absolute (and fixed, which resolves against the viewport): the static
153
+ * position stands in for every `auto` inset. */
154
+ float x = box->x;
155
+ float y = box->y;
156
+ if (!isAuto(style.left)) x = box->cb_x + px(style.left, box->cb_width, em) + box->margin_left;
157
+ else if (!isAuto(style.right)) {
158
+ x = box->cb_x + box->cb_width - px(style.right, box->cb_width, em) - box->margin_right - box->width;
159
+ }
160
+ if (!isAuto(style.top)) y = box->cb_y + px(style.top, box->cb_height, em) + box->margin_top;
161
+ else if (!isAuto(style.bottom)) {
162
+ y = box->cb_y + box->cb_height - px(style.bottom, box->cb_height, em) - box->margin_bottom - box->height;
163
+ }
164
+ translate(box, x - box->x, y - box->y);
165
+ }
166
+
94
167
  float Layouter::layoutBlock(LayoutBox *box, float x, float y, float availableWidth,
95
168
  float availableHeight, float forcedContentWidth,
96
- float forcedContentHeight) const {
169
+ float forcedContentHeight, bool inFlow) const {
97
170
  const XtStyle &style = *box->style;
98
171
  float em = style.font_size;
99
172
  resolveEdges(box, availableWidth);
100
173
 
101
174
  float contentWidth;
175
+ bool outOfFlow = style.position == Position::Absolute || style.position == Position::Fixed;
102
176
  if (forcedContentWidth >= 0) {
103
177
  contentWidth = forcedContentWidth;
104
178
  } else if (isAuto(style.width)) {
105
- contentWidth = availableWidth - horizontalEdges(box);
179
+ if (outOfFlow && !isAuto(style.left) && !isAuto(style.right)) {
180
+ /* Both horizontal insets are set, so `auto` fills the space between them
181
+ * (the padding box of the containing block). */
182
+ contentWidth = box->cb_width - horizontalEdges(box) - px(style.left, box->cb_width, em) -
183
+ px(style.right, box->cb_width, em);
184
+ if (contentWidth < 0) contentWidth = 0;
185
+ } else if (outOfFlow) {
186
+ /* An out-of-flow box does not fill its containing block: an `auto` width
187
+ * is shrink-to-fit — its preferred width, capped by the space available
188
+ * (the containing block's width, not the parent's content box, because an
189
+ * absolute box is laid out against the former). */
190
+ float available = box->cb_width - horizontalEdges(box);
191
+ float preferred = preferredContentWidth(box, availableWidth);
192
+ contentWidth = preferred < available ? preferred : available;
193
+ if (contentWidth < 0) contentWidth = 0;
194
+ } else {
195
+ contentWidth = availableWidth - horizontalEdges(box);
196
+ }
106
197
  } else {
107
198
  contentWidth = px(style.width, availableWidth, em);
108
199
  }
@@ -142,9 +233,32 @@ float Layouter::layoutBlock(LayoutBox *box, float x, float y, float availableWid
142
233
  box->height =
143
234
  box->border_top + box->padding_top + contentHeight + box->padding_bottom + box->border_bottom;
144
235
  box->baseline = box->height;
236
+ /* Now that this box has its final size, its padding box is the containing
237
+ * block its own children resolve against — and only now can an out-of-flow
238
+ * child be placed with `bottom`/`right`. */
239
+ publishContainingBlock(box);
240
+ layoutAbsoluteChildren(box, contentWidth, availableHeight);
241
+ applyPosition(box);
242
+ /* An absolutely positioned box consumes no space in its parent's flow. */
243
+ if (!inFlow) return 0;
145
244
  return box->margin_top + box->height + box->margin_bottom;
146
245
  }
147
246
 
247
+ bool Layouter::isAbsoluteChild(const LayoutBox *box) {
248
+ return box->style != nullptr && (box->style->position == Position::Absolute ||
249
+ box->style->position == Position::Fixed);
250
+ }
251
+
252
+ void Layouter::layoutAbsoluteChildren(LayoutBox *block, float contentWidth,
253
+ float availableHeight) const {
254
+ for (const std::unique_ptr<LayoutBox> &child : block->children) {
255
+ if (child->display == Display::None || !isAbsoluteChild(child.get())) continue;
256
+ setContainingBlock(child.get(), block);
257
+ layoutBlock(child.get(), block->content_x, block->content_y, contentWidth, availableHeight, -1,
258
+ -1, false);
259
+ }
260
+ }
261
+
148
262
  float Layouter::layoutChildren(LayoutBox *block, float contentWidth, float availableHeight,
149
263
  float definiteHeight) const {
150
264
  if (block->style->display == Display::Flex)
@@ -155,14 +269,30 @@ float Layouter::layoutChildren(LayoutBox *block, float contentWidth, float avail
155
269
  std::vector<LayoutBox *> inlineRun;
156
270
  auto flush = [&]() {
157
271
  if (inlineRun.empty()) return;
158
- y += layoutInlineRun(block, inlineRun, contentWidth, y);
272
+ /* Source whitespace between block boxes collapses away (HTML's model), so a
273
+ * run that carries no visible content produces no line at all. */
274
+ bool visible = false;
275
+ for (LayoutBox *box : inlineRun) {
276
+ if (!isCollapsibleText(box)) {
277
+ visible = true;
278
+ break;
279
+ }
280
+ }
281
+ if (visible) y += layoutInlineRun(block, inlineRun, contentWidth, y);
159
282
  inlineRun.clear();
160
283
  };
161
284
  for (const std::unique_ptr<LayoutBox> &child : block->children) {
162
285
  Display display = child->display;
163
286
  if (display == Display::None) continue;
164
- if (display == Display::Block || display == Display::ListItem || display == Display::Flex) {
287
+ if (isAbsoluteChild(child.get())) {
288
+ /* Out of flow: `layoutAbsoluteChildren` places it once this block knows
289
+ * its own size. */
290
+ continue;
291
+ }
292
+ if (display == Display::Block || display == Display::ListItem || display == Display::Flex ||
293
+ child->style == nullptr) {
165
294
  flush();
295
+ setContainingBlock(child.get(), containingBlockAncestor(block));
166
296
  y += layoutBlock(child.get(), block->content_x, y, contentWidth, availableHeight);
167
297
  } else {
168
298
  inlineRun.push_back(child.get());
@@ -62,6 +62,17 @@ inline bool isAuto(const Length &length) {
62
62
  return length.unit == Unit::Auto || length.unit == Unit::Invalid || length.unit == Unit::None;
63
63
  }
64
64
 
65
+ /** True for a text node that holds nothing but HTML source whitespace. HTML
66
+ * collapses those away between block-level boxes, which is what keeps a
67
+ * document's indentation from becoming part of the page (the browser model). */
68
+ inline bool isCollapsibleText(const LayoutBox *box) {
69
+ if (!box->is_text) return false;
70
+ for (char c : box->text) {
71
+ if (c != ' ' && c != '\t' && c != '\n' && c != '\r') return false;
72
+ }
73
+ return true;
74
+ }
75
+
65
76
  /* Per-character advance as a fraction of the font size is no longer needed
66
77
  * here: text metrics come from the shaping stack (see `text.h`). */
67
78
 
@@ -117,14 +128,42 @@ struct Layouter {
117
128
 
118
129
  void resolveEdges(LayoutBox *box, float containingWidth) const;
119
130
 
120
- /** Lay out `box` at (x, y) and return its outer height (margins included). */
131
+ /** Lay out `box` at (x, y) and return its outer height (margins included).
132
+ * `inFlow` is false for an absolutely positioned child: it is still laid out,
133
+ * but the caller ignores the returned height (it is out of flow). */
121
134
  float layoutBlock(LayoutBox *box, float x, float y, float availableWidth, float availableHeight,
122
- float forcedContentWidth = -1, float forcedContentHeight = -1) const;
135
+ float forcedContentWidth = -1, float forcedContentHeight = -1,
136
+ bool inFlow = true) const;
123
137
 
124
138
  /** Lay out the children of `block`; returns the used content height. */
125
139
  float layoutChildren(LayoutBox *block, float contentWidth, float availableHeight,
126
140
  float definiteHeight) const;
127
141
 
142
+ /** Lay out the absolutely positioned children of `block`. Called once the
143
+ * block has its final size, because those children resolve `bottom`/`right`
144
+ * against its padding box. */
145
+ void layoutAbsoluteChildren(LayoutBox *block, float contentWidth, float availableHeight) const;
146
+
147
+ /** True when `box` establishes a containing block for absolute descendants. */
148
+ static bool establishesContainingBlock(const LayoutBox *box);
149
+
150
+ /** True for an out-of-flow child (`position: absolute` or `fixed`). */
151
+ static bool isAbsoluteChild(const LayoutBox *box);
152
+
153
+ /** Nearest ancestor (or `box` itself) that is a containing block, else null. */
154
+ const LayoutBox *containingBlockAncestor(const LayoutBox *box) const;
155
+
156
+ /** Re-publish the containing block of `box`'s children once `box` has its own
157
+ * final size (an absolute child resolves `bottom`/`right` against it). */
158
+ void publishContainingBlock(LayoutBox *box) const;
159
+
160
+ /** Publish `box`'s padding box as the containing block for absolute
161
+ * descendants of the nearest positioned ancestor (null means the viewport). */
162
+ void setContainingBlock(LayoutBox *box, const LayoutBox *ancestor) const;
163
+
164
+ /** Resolve the static/relative/absolute offsets of an already laid-out box. */
165
+ void applyPosition(LayoutBox *box) const;
166
+
128
167
  void flattenInline(LayoutBox *box, float contentWidth, std::vector<InlineItem> &out) const;
129
168
 
130
169
  void computeInlineGeometry(LayoutBox *box) const;
@@ -48,6 +48,7 @@ void Layouter::flattenInline(LayoutBox *box, float contentWidth,
48
48
  if (box->display == Display::InlineBlock) {
49
49
  float available = contentWidth;
50
50
  float intrinsic = preferredContentWidth(box, available);
51
+ setContainingBlock(box, containingBlockAncestor(box));
51
52
  layoutBlock(box, 0, 0, available, -1, intrinsic);
52
53
  InlineItem item;
53
54
  item.box = box;
@@ -163,9 +164,16 @@ float Layouter::layoutInlineRun(LayoutBox *container, const std::vector<LayoutBo
163
164
  }
164
165
  for (InlineItem *item : current) {
165
166
  if (item->atomic) {
166
- float dx = x + item->box->margin_left - item->box->x;
167
- float dy = cursorY + item->box->margin_top - item->box->y;
168
- translate(item->box, dx, dy);
167
+ /* An out-of-flow item was already placed against its containing block by
168
+ * `layoutBlock`, so it must not be moved back onto the line. */
169
+ if (!isAbsoluteChild(item->box)) {
170
+ float dx = x + item->box->margin_left - item->box->x;
171
+ float dy = cursorY + item->box->margin_top - item->box->y;
172
+ translate(item->box, dx, dy);
173
+ /* `position: relative` on an inline-block shifts it after line
174
+ * placement, so the line box keeps the space it reserved. */
175
+ applyPosition(item->box);
176
+ }
169
177
  } else {
170
178
  item->box->fragments.push_back({x, cursorY, item->width, height, item->text});
171
179
  }
@@ -67,6 +67,12 @@ void paintBox(const LayoutBox *box, DisplayList &out) {
67
67
  if (box == nullptr) return;
68
68
  const XtStyle *style = box->style;
69
69
 
70
+ /* A fully transparent box paints nothing — not itself, and not its subtree.
71
+ * `opacity` is otherwise unsupported: a shape carries its colour's alpha only,
72
+ * so a partly transparent box (an `opacity` transition mid-flight, say) is
73
+ * still painted opaque. */
74
+ if (style != nullptr && style->opacity <= 0.0f) return;
75
+
70
76
  if (style != nullptr && box->display != Display::None) {
71
77
  if (box->is_text) {
72
78
  addTextRuns(box, *style, out);
@@ -130,30 +130,32 @@ bool xt_gui_renderer_ensure(SDL_GPUDevice *device, SDL_Window *reference) {
130
130
  using namespace xtgui::renderer_detail;
131
131
  if (g_pipeline != nullptr) return true;
132
132
  if (g_renderer_failed) return false;
133
- if ((SDL_GetGPUShaderFormats(device) & SDL_GPU_SHADERFORMAT_MSL) == 0) {
133
+ const ShaderSource fill_vs = selectShader(device, ShaderProgram::Fill, false);
134
+ const ShaderSource fill_fs = selectShader(device, ShaderProgram::Fill, true);
135
+ if (!fill_vs.valid() || !fill_fs.valid()) {
134
136
  fprintf(stderr,
135
- "xt_gui: backend has no MSL shader support; shape/text painting disabled "
136
- "(Vulkan/D3D12 shader blobs are a TODO)\n");
137
+ "xt_gui: backend supports neither Metal nor D3D12 shaders; shape/text painting "
138
+ "disabled (Vulkan SPIR-V blobs are a TODO)\n");
137
139
  g_renderer_failed = 1;
138
140
  return false;
139
141
  }
140
142
 
141
143
  SDL_GPUShaderCreateInfo vs_info;
142
144
  memset(&vs_info, 0, sizeof(vs_info));
143
- vs_info.code_size = strlen(kFillMSL);
144
- vs_info.code = (const Uint8 *)kFillMSL;
145
- vs_info.entrypoint = "vs_main";
146
- vs_info.format = SDL_GPU_SHADERFORMAT_MSL;
145
+ vs_info.code_size = fill_vs.size;
146
+ vs_info.code = fill_vs.code;
147
+ vs_info.entrypoint = fill_vs.entry;
148
+ vs_info.format = fill_vs.format;
147
149
  vs_info.stage = SDL_GPU_SHADERSTAGE_VERTEX;
148
150
  vs_info.num_uniform_buffers = 1;
149
151
  SDL_GPUShader *vs = SDL_CreateGPUShader(device, &vs_info);
150
152
 
151
153
  SDL_GPUShaderCreateInfo fs_info;
152
154
  memset(&fs_info, 0, sizeof(fs_info));
153
- fs_info.code_size = strlen(kFillMSL);
154
- fs_info.code = (const Uint8 *)kFillMSL;
155
- fs_info.entrypoint = "fs_main";
156
- fs_info.format = SDL_GPU_SHADERFORMAT_MSL;
155
+ fs_info.code_size = fill_fs.size;
156
+ fs_info.code = fill_fs.code;
157
+ fs_info.entrypoint = fill_fs.entry;
158
+ fs_info.format = fill_fs.format;
157
159
  fs_info.stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
158
160
  SDL_GPUShader *fs = SDL_CreateGPUShader(device, &fs_info);
159
161
 
@@ -17,22 +17,26 @@ namespace xtgui {
17
17
  namespace renderer_detail {
18
18
 
19
19
  SDL_GPUGraphicsPipeline *createImagePipeline(SDL_GPUDevice *device, SDL_Window *reference) {
20
+ const ShaderSource image_vs = selectShader(device, ShaderProgram::Image, false);
21
+ const ShaderSource image_fs = selectShader(device, ShaderProgram::Image, true);
22
+ if (!image_vs.valid() || !image_fs.valid()) return nullptr;
23
+
20
24
  SDL_GPUShaderCreateInfo vs_info;
21
25
  memset(&vs_info, 0, sizeof(vs_info));
22
- vs_info.code_size = strlen(kTextMSL);
23
- vs_info.code = (const Uint8 *)kTextMSL;
24
- vs_info.entrypoint = "vs_text";
25
- vs_info.format = SDL_GPU_SHADERFORMAT_MSL;
26
+ vs_info.code_size = image_vs.size;
27
+ vs_info.code = image_vs.code;
28
+ vs_info.entrypoint = image_vs.entry;
29
+ vs_info.format = image_vs.format;
26
30
  vs_info.stage = SDL_GPU_SHADERSTAGE_VERTEX;
27
31
  vs_info.num_uniform_buffers = 1;
28
32
  SDL_GPUShader *vs = SDL_CreateGPUShader(device, &vs_info);
29
33
 
30
34
  SDL_GPUShaderCreateInfo fs_info;
31
35
  memset(&fs_info, 0, sizeof(fs_info));
32
- fs_info.code_size = strlen(kTextMSL);
33
- fs_info.code = (const Uint8 *)kTextMSL;
34
- fs_info.entrypoint = "fs_image";
35
- fs_info.format = SDL_GPU_SHADERFORMAT_MSL;
36
+ fs_info.code_size = image_fs.size;
37
+ fs_info.code = image_fs.code;
38
+ fs_info.entrypoint = image_fs.entry;
39
+ fs_info.format = image_fs.format;
36
40
  fs_info.stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
37
41
  fs_info.num_samplers = 1;
38
42
  SDL_GPUShader *fs = SDL_CreateGPUShader(device, &fs_info);
@@ -1,13 +1,39 @@
1
1
  /*
2
- * xbintsc GUI engine — SDL_GPU Metal (MSL) shader sources.
2
+ * xbintsc GUI engine — shader sources.
3
3
  *
4
- * Shared by `renderer.cpp` (fills), `renderer_text.cpp` (glyphs) and
5
- * `renderer_image.cpp` (images). Text and image quads reuse `kTextMSL`; only
6
- * the fragment entry point differs (`fs_text` vs `fs_image`).
4
+ * `renderer.cpp` (fills), `renderer_text.cpp` (glyphs) and `renderer_image.cpp`
5
+ * (images) each build one SDL_GPU graphics pipeline. Three shader families back
6
+ * them, one per SDL_GPU backend:
7
+ *
8
+ * - Metal: the MSL string literals below, which SDL_GPU compiles at runtime.
9
+ * - Vulkan: SPIR-V blobs in `renderer_shaders_data.h`, compiled from
10
+ * `spirv/*.{vert,frag}` by `scripts/build-gui-shaders.mjs`.
11
+ * - Direct3D 12: DXIL blobs from the same header, compiled from `shaders.hlsl`.
12
+ *
13
+ * `selectShader` resolves a logical program and stage to whichever of those the
14
+ * device accepts. The backend rejects any other format, so a missing blob means
15
+ * "no pipeline", never a mismatched one.
16
+ *
17
+ * Text and image quads share the vertex program; only the fragment entry point
18
+ * differs (`fs_text` vs `fs_image`).
19
+ *
20
+ * Note the entry-point names differ per format: MSL/DXIL keep the descriptive
21
+ * names from their sources, while glslc emits the GLSL `main`.
7
22
  */
8
23
  #ifndef XT_GUI_RENDERER_SHADERS_H
9
24
  #define XT_GUI_RENDERER_SHADERS_H
10
25
 
26
+ #include <SDL3/SDL.h>
27
+ #include <cstring>
28
+
29
+ #include "renderer_shaders_data.h"
30
+
31
+ namespace xtgui {
32
+ namespace renderer_detail {
33
+
34
+ /** The three pipeline programs the engine builds. */
35
+ enum class ShaderProgram { Fill, Text, Image };
36
+
11
37
  /* Rounded-rectangle fill. Vertex attributes (matching SDL_GPUVertexAttribute):
12
38
  * 0 -> position (pixels, viewport-relative)
13
39
  * 1 -> local (pixels, relative to the rectangle centre)
@@ -105,4 +131,105 @@ fragment float4 fs_image(TextOut in [[stage_in]],
105
131
  }
106
132
  )MSL";
107
133
 
134
+ /** One stage of one pipeline program, in whichever format the device accepts. */
135
+ struct ShaderSource {
136
+ SDL_GPUShaderFormat format = 0;
137
+ const unsigned char *code = nullptr;
138
+ size_t size = 0;
139
+ const char *entry = nullptr;
140
+
141
+ bool valid() const { return code != nullptr; }
142
+ };
143
+
144
+ /** The MSL text backing `program` (one literal covers both stages). */
145
+ inline const char *mslSource(ShaderProgram program) {
146
+ return program == ShaderProgram::Fill ? kFillMSL : kTextMSL;
147
+ }
148
+
149
+ /** MSL entry point names for `program`, matching the literals below. */
150
+ inline const char *vertexEntry(ShaderProgram program) {
151
+ return program == ShaderProgram::Fill ? "vs_main" : "vs_text";
152
+ }
153
+
154
+ inline const char *fragmentEntry(ShaderProgram program) {
155
+ if (program == ShaderProgram::Fill) return "fs_main";
156
+ return program == ShaderProgram::Text ? "fs_text" : "fs_image";
157
+ }
158
+
159
+ /** A blob pair for one shader format: bytes plus the entry-point names it uses.
160
+ * The names are stored in `ShaderSource` rather than resolved at the call site
161
+ * because cross-compilers rename entry points (glslc emits `main`, dxc keeps the
162
+ * HLSL name, MSL keeps the Metal name). */
163
+ struct ShaderBlobs {
164
+ const unsigned char *vertex;
165
+ size_t vertex_size;
166
+ const char *vertex_entry;
167
+ const unsigned char *fragment;
168
+ size_t fragment_size;
169
+ const char *fragment_entry;
170
+ };
171
+
172
+ /** SPIR-V blobs for `program` (Vulkan). glslc names every entry point `main`. */
173
+ inline ShaderBlobs spirvBlobs(ShaderProgram program) {
174
+ switch (program) {
175
+ case ShaderProgram::Fill:
176
+ return {kFillSpirvVs, kFillSpirvVsSize, "main", kFillSpirvFs, kFillSpirvFsSize, "main"};
177
+ case ShaderProgram::Text:
178
+ return {kTextSpirvVs, kTextSpirvVsSize, "main", kTextSpirvFs, kTextSpirvFsSize, "main"};
179
+ case ShaderProgram::Image:
180
+ return {kTextSpirvVs, kTextSpirvVsSize, "main", kImageSpirvFs, kImageSpirvFsSize, "main"};
181
+ }
182
+ return {nullptr, 0, nullptr, nullptr, 0, nullptr};
183
+ }
184
+
185
+ /** DXIL blobs for `program` (Direct3D 12); one blob per entry point. */
186
+ inline ShaderBlobs dxilBlobs(ShaderProgram program) {
187
+ switch (program) {
188
+ case ShaderProgram::Fill:
189
+ return {kFillDxilVs, kFillDxilVsSize, "vs_main", kFillDxilFs, kFillDxilFsSize, "fs_main"};
190
+ case ShaderProgram::Text:
191
+ return {kTextDxilVs, kTextDxilVsSize, "vs_text", kTextDxilFs, kTextDxilFsSize, "fs_text"};
192
+ case ShaderProgram::Image:
193
+ return {kTextDxilVs, kTextDxilVsSize, "vs_text", kImageDxilFs, kImageDxilFsSize, "fs_image"};
194
+ }
195
+ return {nullptr, 0, nullptr, nullptr, 0, nullptr};
196
+ }
197
+
198
+ /** MSL for `program`: a single source holds both stages, so it is expressed as a
199
+ * blob pair sharing one buffer. */
200
+ inline ShaderBlobs mslBlobs(ShaderProgram program) {
201
+ const char *msl = mslSource(program);
202
+ const size_t size = strlen(msl);
203
+ const unsigned char *code = (const unsigned char *)msl;
204
+ return {code, size, vertexEntry(program), code, size, fragmentEntry(program)};
205
+ }
206
+
207
+ /** Resolve `program`'s vertex and fragment stages to shaders the device accepts.
208
+ * Returns an invalid source when the backend supports none of the formats the
209
+ * engine ships. */
210
+ inline ShaderSource selectShader(SDL_GPUDevice *device, ShaderProgram program, bool fragment) {
211
+ ShaderSource source;
212
+ const SDL_GPUShaderFormat formats = SDL_GetGPUShaderFormats(device);
213
+ ShaderBlobs blobs;
214
+ if ((formats & SDL_GPU_SHADERFORMAT_MSL) != 0) {
215
+ source.format = SDL_GPU_SHADERFORMAT_MSL;
216
+ blobs = mslBlobs(program);
217
+ } else if ((formats & SDL_GPU_SHADERFORMAT_SPIRV) != 0) {
218
+ source.format = SDL_GPU_SHADERFORMAT_SPIRV;
219
+ blobs = spirvBlobs(program);
220
+ } else if ((formats & SDL_GPU_SHADERFORMAT_DXIL) != 0) {
221
+ source.format = SDL_GPU_SHADERFORMAT_DXIL;
222
+ blobs = dxilBlobs(program);
223
+ } else {
224
+ return source;
225
+ }
226
+ source.code = fragment ? blobs.fragment : blobs.vertex;
227
+ source.size = fragment ? blobs.fragment_size : blobs.vertex_size;
228
+ source.entry = fragment ? blobs.fragment_entry : blobs.vertex_entry;
229
+ return source;
230
+ }
231
+
232
+ } // namespace renderer_detail
233
+ } // namespace xtgui
234
+
108
235
  #endif /* XT_GUI_RENDERER_SHADERS_H */