xbintsc 0.3.49 → 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 (48) hide show
  1. package/dist/src/driver/compiler.js +25 -1
  2. package/dist/src/driver/compiler.js.map +1 -1
  3. package/dist/src/driver/config.d.ts +7 -0
  4. package/dist/src/driver/config.js.map +1 -1
  5. package/dist/src/driver/win-entry.d.ts +21 -0
  6. package/dist/src/driver/win-entry.js +52 -0
  7. package/dist/src/driver/win-entry.js.map +1 -0
  8. package/dist/tests/e2e/gui-example-browser.test.d.ts +12 -0
  9. package/dist/tests/e2e/gui-example-browser.test.js +164 -0
  10. package/dist/tests/e2e/gui-example-browser.test.js.map +1 -0
  11. package/dist/tests/e2e/gui-example.test.d.ts +14 -0
  12. package/dist/tests/e2e/gui-example.test.js +76 -0
  13. package/dist/tests/e2e/gui-example.test.js.map +1 -0
  14. package/dist/tests/e2e/gui-helpers.d.ts +39 -4
  15. package/dist/tests/e2e/gui-helpers.js +38 -9
  16. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  17. package/dist/tests/e2e/gui-layout.test.js +155 -0
  18. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  19. package/dist/tests/e2e/gui-lifecycle.test.js +10 -1
  20. package/dist/tests/e2e/gui-lifecycle.test.js.map +1 -1
  21. package/dist/tests/e2e/gui-paint.test.js +44 -4
  22. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  23. package/dist/tests/e2e/gui-script.test.js +8 -4
  24. package/dist/tests/e2e/gui-script.test.js.map +1 -1
  25. package/doc/gui.md +113 -7
  26. package/doc/xbintsc.config.schema.json +4 -0
  27. package/doc/zh-CN/gui.md +74 -6
  28. package/package.json +1 -1
  29. package/runtime/ext_gui/dom_api.cpp +19 -0
  30. package/runtime/ext_gui/dom_api_node.cpp +4 -1
  31. package/runtime/ext_gui/dom_api_node_read.cpp +12 -0
  32. package/runtime/ext_gui/gui.cpp +42 -2
  33. package/runtime/ext_gui/gui_engine.h +15 -0
  34. package/runtime/ext_gui/gui_events.cpp +10 -3
  35. package/runtime/ext_gui/layout.cpp +6 -0
  36. package/runtime/ext_gui/layout.h +4 -0
  37. package/runtime/ext_gui/layout_flex.cpp +13 -5
  38. package/runtime/ext_gui/layout_flow.cpp +134 -4
  39. package/runtime/ext_gui/layout_flow_internal.h +41 -2
  40. package/runtime/ext_gui/layout_inline.cpp +11 -3
  41. package/runtime/ext_gui/paint.cpp +6 -0
  42. package/runtime/ext_gui/script.cpp +12 -2
  43. package/runtime/ext_gui/text.cpp +14 -0
  44. package/runtime/ext_gui/window.cpp +12 -0
  45. package/runtime/rt.h +10 -0
  46. package/src/driver/compiler.ts +26 -1
  47. package/src/driver/config.ts +7 -0
  48. package/src/driver/win-entry.ts +59 -0
@@ -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);
@@ -15,8 +15,11 @@
15
15
  #include <unordered_map>
16
16
  #include <vector>
17
17
 
18
- /** Script id -> AOT-compiled function value. Lives for the whole process (the
19
- * runtime arena never reclaims, so storing `xt_value`s is safe). */
18
+ /** Script id -> AOT-compiled function value. Lives for the whole process. The
19
+ * runtime collector is a mark-sweep over the heap, so these values are only
20
+ * safe while a root provider keeps them reachable — `xt_gui_script_gc_scan`
21
+ * below does that (without it a collection frees everything the script
22
+ * captured and the first frame after it crashes). */
20
23
  static std::unordered_map<std::string, xt_value> &script_registry() {
21
24
  static std::unordered_map<std::string, xt_value> registry;
22
25
  return registry;
@@ -31,9 +34,16 @@ extern "C" xt_value xt_register_script(int32_t argc, xt_value *argv) {
31
34
  script_registry()[std::string(id)] = fn;
32
35
  }
33
36
  }
37
+ /* Registration is idempotent; do it on first use so a program that never
38
+ * opens a window still keeps its scripts alive across a collection. */
39
+ xt_gc_register_root_provider(xt_gui_script_gc_scan);
34
40
  return XT_UNDEFINED;
35
41
  }
36
42
 
43
+ void xt_gui_script_gc_scan(void) {
44
+ for (const auto &entry : script_registry()) xt_gc_mark_value(entry.second);
45
+ }
46
+
37
47
  /** Collect `data-xt-id` markers from `<script>` elements, in document order. */
38
48
  static void collect_script_ids(const xtgui::Node *node, std::vector<std::string> &ids) {
39
49
  if (node == nullptr) return;
@@ -46,10 +46,21 @@ namespace {
46
46
  const char *const kSansCandidates[] = {
47
47
  /* Explicit override wins (also checked per family class below). */
48
48
  nullptr,
49
+ /* Fonts with CJK coverage come first: the default face is a single face, so
50
+ * it has to be able to render Chinese/Japanese/Korean text as well as
51
+ * Latin (see "Implemented text" in doc/gui.md). */
52
+ "C:\\Windows\\Fonts\\msyh.ttc", /* Microsoft YaHei (Win 8.1+) */
53
+ "C:\\Windows\\Fonts\\msyh.ttf", /* Microsoft YaHei (Win 7) */
54
+ "C:\\Windows\\Fonts\\Deng.ttf", /* DengXian */
55
+ "C:\\Windows\\Fonts\\simsun.ttc", /* SimSun */
56
+ "/System/Library/Fonts/PingFang.ttc",
57
+ "/System/Library/Fonts/STHeiti Light.ttc",
49
58
  "/System/Library/Fonts/Helvetica.ttc",
50
59
  "/System/Library/Fonts/Supplemental/Arial.ttf",
51
60
  "/System/Library/Fonts/Supplemental/Verdana.ttf",
52
61
  "/Library/Fonts/Arial.ttf",
62
+ "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
63
+ "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
53
64
  "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
54
65
  "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
55
66
  "/usr/share/fonts/TTF/DejaVuSans.ttf",
@@ -60,10 +71,13 @@ const char *const kSansCandidates[] = {
60
71
 
61
72
  const char *const kMonoCandidates[] = {
62
73
  nullptr,
74
+ "C:\\Windows\\Fonts\\msyh.ttc", /* CJK-capable fallback (see above) */
75
+ "/System/Library/Fonts/PingFang.ttc",
63
76
  "/System/Library/Fonts/Menlo.ttc",
64
77
  "/System/Library/Fonts/Supplemental/Courier New.ttf",
65
78
  "/System/Library/Fonts/Courier.ttc",
66
79
  "/Library/Fonts/Courier New.ttf",
80
+ "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
67
81
  "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
68
82
  "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
69
83
  "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
@@ -283,6 +283,17 @@ static xt_value win_is_open(xt_value self, xt_value env, int32_t argc, xt_value
283
283
  return (win != NULL && win->open) ? XT_TRUE : XT_FALSE;
284
284
  }
285
285
 
286
+ /** `/driver`: which SDL_GPU backend the device is running (`"vulkan"`,
287
+ * `"direct3d12"`, `"metal"`, …). The engine asks SDL for the best available
288
+ * one, so this is how a program (or a test) confirms the path in use. */
289
+ static xt_value win_driver(xt_value self, xt_value env, int32_t argc, xt_value *argv) {
290
+ (void)self;
291
+ (void)env;
292
+ (void)argc;
293
+ (void)argv;
294
+ return xt_string_from_cstr(xt_gui_driver());
295
+ }
296
+
286
297
  /** `requestAnimationFrame(fn)` -> id. The callback runs once on the next frame
287
298
  * with the frame timestamp (ms) as its only argument. */
288
299
  static xt_value win_request_animation_frame(xt_value self, xt_value env, int32_t argc,
@@ -455,6 +466,7 @@ xt_value xt_gui_window_proto(void) {
455
466
  define_method(proto, "fontMetrics", (void *)win_font_metrics);
456
467
  define_method(proto, "close", (void *)win_close);
457
468
  define_method(proto, "isOpen", (void *)win_is_open);
469
+ define_method(proto, "driver", (void *)win_driver);
458
470
  define_method(proto, "requestAnimationFrame", (void *)win_request_animation_frame);
459
471
  define_method(proto, "cancelAnimationFrame", (void *)win_cancel_animation_frame);
460
472
  define_method(proto, "on", (void *)win_on);
package/runtime/rt.h CHANGED
@@ -371,6 +371,16 @@ size_t xt_heap_allocations(void);
371
371
  */
372
372
  void xt_gc_add_root(xt_value *slot);
373
373
  void xt_gc_add_root_range(xt_value *base, size_t count);
374
+ /**
375
+ * Register a provider that marks a subsystem's live set when the values are
376
+ * not reachable through value slots (the event loop's timers, a promise
377
+ * microtask queue, an extension that caches values in its own containers).
378
+ * Providers run during every collection and registration is idempotent.
379
+ */
380
+ void xt_gc_register_root_provider(void (*provider)(void));
381
+ /** Mark `value` (and everything reachable from it) as live. Root providers use
382
+ * this; the collector also marks explicitly registered slots and the C stack. */
383
+ void xt_gc_mark_value(xt_value value);
374
384
 
375
385
  #ifdef __cplusplus
376
386
  }
@@ -23,6 +23,7 @@ import { findRuntimeLibrary } from "./runtime-lib.js";
23
23
  import { resolveToolchain } from "./toolchain-provider.js";
24
24
  import { ensureIconObject, readIcon, EMPTY_ICON, IconError, type IconInfo } from "./icon.js";
25
25
  import { ensureWindowsIconResource, resolveResourceCompiler } from "./win-icon.js";
26
+ import { ensureWindowsEntryObject } from "./win-entry.js";
26
27
  import { bundlePathFor, packageMacApp } from "./mac-bundle.js";
27
28
  import type { AppConfig } from "./config.js";
28
29
  import {
@@ -295,6 +296,18 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
295
296
  toolchain.env,
296
297
  );
297
298
 
299
+ /* On Windows a GUI program links as a **Windows-subsystem** executable, so
300
+ * launching it does not flash a console window. That subsystem's C runtime
301
+ * wants `WinMain`, so a tiny shim object bridges it to the runtime's `main`.
302
+ * `app.console: true` keeps the console subsystem instead (useful while
303
+ * debugging, because stdout and stderr only exist there). Other platforms
304
+ * have no equivalent. */
305
+ const windowsSubsystem = process.platform === "win32" && registry.has("gui") && app.console !== true;
306
+ const consoleSubsystem = windowsSubsystem ? ["-Wl,/SUBSYSTEM:WINDOWS"] : [];
307
+ const entryObject = windowsSubsystem
308
+ ? ensureWindowsEntryObject(runner, clang, cacheDir, toolchain.env)
309
+ : undefined;
310
+
298
311
  const iconObjects: string[] = [];
299
312
  // The GUI extension references the icon symbols unconditionally, so a GUI
300
313
  // program gets an (empty) icon object even with no icon configured.
@@ -320,12 +333,24 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
320
333
 
321
334
  link(runner, {
322
335
  clang,
323
- objectPaths: [objectPath, ...runtimeObjects, ...extensionObjects, ...nativeObjects, ...iconObjects],
336
+ objectPaths: [
337
+ objectPath,
338
+ ...runtimeObjects,
339
+ ...extensionObjects,
340
+ ...nativeObjects,
341
+ ...(entryObject !== undefined ? [entryObject] : []),
342
+ ...iconObjects,
343
+ ],
324
344
  outputPath,
325
345
  linkerFlags: [
326
346
  ...toolchain.linkerArgs,
327
347
  ...(process.platform === "win32" ? ["-lws2_32"] : ["-lm"]),
328
348
  ...registry.linkerFlags(),
349
+ /* A GUI program is a Windows-subsystem executable: no console window is
350
+ * created when it is launched. Console output (stdout/stderr) is lost in
351
+ * that subsystem, so a program that wants to report things opts back in
352
+ * with `app.console: true`. */
353
+ ...consoleSubsystem,
329
354
  ],
330
355
  optimize,
331
356
  env: toolchain.env,
@@ -24,6 +24,13 @@ export interface AppConfig {
24
24
  /** macOS only: produce a `<name>.app` bundle. */
25
25
  readonly bundle?: boolean;
26
26
  readonly bundleId?: string;
27
+ /**
28
+ * Windows only: link as a console-subsystem executable instead of the default
29
+ * Windows subsystem. A GUI program defaults to the Windows subsystem so that
30
+ * launching it does not open a console window — which also means stdout and
31
+ * stderr go nowhere, so set this while debugging a GUI program.
32
+ */
33
+ readonly console?: boolean;
27
34
  }
28
35
 
29
36
  /** The on-disk schema of `xbintsc.config.json`. Every field is optional. */
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Windows GUI-subsystem entry point.
3
+ *
4
+ * A program that links with the `gui` extension is built as a **Windows
5
+ * subsystem** executable (`/SUBSYSTEM:WINDOWS`), which is what keeps a console
6
+ * window from flashing up when it is launched. The MSVC C runtime then expects
7
+ * `WinMain` instead of `main`, and xbintsc's runtime always emits a C `main`
8
+ * (the program body plus teardown), so the driver links this tiny shim to
9
+ * bridge the two.
10
+ *
11
+ * It is compiled on demand and cached like the icon object; nothing here is
12
+ * platform-abstract — on every other platform the file is never used.
13
+ */
14
+
15
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { hashParts } from "./cache.js";
18
+ import { compileC, type Runner } from "./toolchain.js";
19
+
20
+ /** The bridge source. `main` is the entry point the runtime emits. */
21
+ export const WINDOWS_ENTRY_SOURCE = `/* Generated by xbintsc - Windows GUI subsystem entry point. Do not edit. */
22
+ #ifndef UNICODE
23
+ #define UNICODE
24
+ #endif
25
+ #include <windows.h>
26
+
27
+ /* The xbintsc runtime defines the C program entry point; hand it the command
28
+ * line minus the argv[0] hole so it still sees its own arguments. */
29
+ int main(int argc, char **argv);
30
+
31
+ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR command_line, int show) {
32
+ (void)instance;
33
+ (void)previous;
34
+ (void)command_line;
35
+ (void)show;
36
+ return main(__argc, __argv);
37
+ }
38
+ `;
39
+
40
+ /**
41
+ * Compile the entry shim into an object file (cached by source + platform +
42
+ * toolchain) and return its path.
43
+ */
44
+ export function ensureWindowsEntryObject(
45
+ runner: Runner,
46
+ clang: string,
47
+ cacheDir: string,
48
+ env?: Record<string, string>,
49
+ ): string {
50
+ const key = hashParts(["win-gui-entry", process.platform, clang, WINDOWS_ENTRY_SOURCE]);
51
+ const sourcePath = join(cacheDir, `winentry-${key}.c`);
52
+ const objectPath = join(cacheDir, `winentry-${key}.o`);
53
+ if (!existsSync(objectPath)) {
54
+ mkdirSync(cacheDir, { recursive: true });
55
+ writeFileSync(sourcePath, WINDOWS_ENTRY_SOURCE);
56
+ compileC(runner, clang, sourcePath, objectPath, cacheDir, env);
57
+ }
58
+ return objectPath;
59
+ }