xbintsc 0.3.49 → 0.3.65

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 (51) 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-events.test.js +32 -0
  9. package/dist/tests/e2e/gui-events.test.js.map +1 -1
  10. package/dist/tests/e2e/gui-example-browser.test.d.ts +12 -0
  11. package/dist/tests/e2e/gui-example-browser.test.js +168 -0
  12. package/dist/tests/e2e/gui-example-browser.test.js.map +1 -0
  13. package/dist/tests/e2e/gui-example.test.d.ts +14 -0
  14. package/dist/tests/e2e/gui-example.test.js +76 -0
  15. package/dist/tests/e2e/gui-example.test.js.map +1 -0
  16. package/dist/tests/e2e/gui-helpers.d.ts +39 -4
  17. package/dist/tests/e2e/gui-helpers.js +38 -9
  18. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  19. package/dist/tests/e2e/gui-layout.test.js +155 -0
  20. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  21. package/dist/tests/e2e/gui-lifecycle.test.js +10 -1
  22. package/dist/tests/e2e/gui-lifecycle.test.js.map +1 -1
  23. package/dist/tests/e2e/gui-paint.test.js +44 -4
  24. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  25. package/dist/tests/e2e/gui-script.test.js +8 -4
  26. package/dist/tests/e2e/gui-script.test.js.map +1 -1
  27. package/doc/gui.md +120 -9
  28. package/doc/xbintsc.config.schema.json +4 -0
  29. package/doc/zh-CN/gui.md +78 -7
  30. package/package.json +1 -1
  31. package/runtime/ext_gui/dom_api.cpp +19 -0
  32. package/runtime/ext_gui/dom_api_node.cpp +4 -1
  33. package/runtime/ext_gui/dom_api_node_read.cpp +12 -0
  34. package/runtime/ext_gui/gui.cpp +42 -2
  35. package/runtime/ext_gui/gui_engine.h +15 -0
  36. package/runtime/ext_gui/gui_events.cpp +13 -3
  37. package/runtime/ext_gui/layout.cpp +6 -0
  38. package/runtime/ext_gui/layout.h +4 -0
  39. package/runtime/ext_gui/layout_flex.cpp +13 -5
  40. package/runtime/ext_gui/layout_flow.cpp +134 -4
  41. package/runtime/ext_gui/layout_flow_internal.h +41 -2
  42. package/runtime/ext_gui/layout_inline.cpp +11 -3
  43. package/runtime/ext_gui/paint.cpp +6 -0
  44. package/runtime/ext_gui/script.cpp +12 -2
  45. package/runtime/ext_gui/text.cpp +14 -0
  46. package/runtime/ext_gui/window.cpp +44 -0
  47. package/runtime/rt.h +10 -0
  48. package/runtime/xt_io/console.inc +15 -3
  49. package/src/driver/compiler.ts +26 -1
  50. package/src/driver/config.ts +7 -0
  51. package/src/driver/win-entry.ts +59 -0
@@ -19,9 +19,12 @@ float Layouter::layoutFlex(LayoutBox *container, float contentWidth, float defin
19
19
  containerStyle.flex_direction == FlexDirection::ColumnReverse;
20
20
  float gap = px(containerStyle.gap, contentWidth, containerStyle.font_size, 0.0f);
21
21
 
22
+ /* An absolutely positioned child is out of flow: it takes no part in the flex
23
+ * algorithm and is placed against the container's padding box instead. */
22
24
  std::vector<LayoutBox *> items;
23
25
  for (const std::unique_ptr<LayoutBox> &child : container->children) {
24
- if (child->display != Display::None) items.push_back(child.get());
26
+ if (child->display == Display::None || isAbsoluteChild(child.get())) continue;
27
+ items.push_back(child.get());
25
28
  }
26
29
  if (items.empty()) return 0;
27
30
  size_t count = items.size();
@@ -71,6 +74,7 @@ float Layouter::layoutFlex(LayoutBox *container, float contentWidth, float defin
71
74
  for (size_t i = 0; i < count; i++) {
72
75
  float contentW = mainSizes[i] - horizontalEdges(items[i]);
73
76
  if (contentW < 0) contentW = 0;
77
+ setContainingBlock(items[i], containingBlockAncestor(items[i]));
74
78
  layoutBlock(items[i], container->content_x, container->content_y, contentWidth,
75
79
  definiteHeight, contentW);
76
80
  cross[i] = verticalEdges(items[i]) + items[i]->height;
@@ -98,8 +102,8 @@ float Layouter::layoutFlex(LayoutBox *container, float contentWidth, float defin
98
102
  for (size_t k = 0; k < count; k++) {
99
103
  size_t i = reverse ? (count - 1 - k) : k;
100
104
  float crossOffset = crossOffsetFor(containerStyle.align_items, crossSize, cross[i]);
101
- float dx = cursor - items[i]->x;
102
- float dy = container->content_y + crossOffset - items[i]->y;
105
+ float dx = cursor + items[i]->margin_left - items[i]->x;
106
+ float dy = container->content_y + crossOffset + items[i]->margin_top - items[i]->y;
103
107
  translate(items[i], dx, dy);
104
108
  cursor += mainSizes[i] + between;
105
109
  }
@@ -111,6 +115,7 @@ float Layouter::layoutFlex(LayoutBox *container, float contentWidth, float defin
111
115
  std::vector<float> outer(count, 0.0f);
112
116
  float sumBase = 0;
113
117
  for (size_t i = 0; i < count; i++) {
118
+ setContainingBlock(items[i], containingBlockAncestor(items[i]));
114
119
  layoutBlock(items[i], container->content_x, container->content_y, contentWidth, definiteHeight);
115
120
  outer[i] = verticalEdges(items[i]) + items[i]->height;
116
121
  sumBase += outer[i];
@@ -142,8 +147,11 @@ float Layouter::layoutFlex(LayoutBox *container, float contentWidth, float defin
142
147
  float cursor = container->content_y + offset;
143
148
  for (size_t k = 0; k < count; k++) {
144
149
  size_t i = reverse ? (count - 1 - k) : k;
145
- float dx = container->content_x - items[i]->x;
146
- float dy = cursor - items[i]->y;
150
+ float dx = container->content_x + items[i]->margin_left - items[i]->x;
151
+ /* Margins keep the item's outer box on the main axis, exactly like block
152
+ * flow — a negative `margin-top` pulls the item up instead of being
153
+ * silently dropped. */
154
+ float dy = cursor + items[i]->margin_top - items[i]->y;
147
155
  translate(items[i], dx, dy);
148
156
  cursor += mainSizes[i] + between;
149
157
  }
@@ -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,28 @@ 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
+ /** Web 'window.closed': true once the window has been closed. The browser
287
+ * spelling of 'isOpen()', so a document can stop its frame loop with
288
+ * 'if (window.closed) return;' in every host. */
289
+ static xt_value win_closed_get(xt_value self, xt_value env, int32_t argc, xt_value *argv) {
290
+ (void)env;
291
+ (void)argc;
292
+ (void)argv;
293
+ XtGuiWindow *win = xt_gui_window_from_this(self);
294
+ return (win != NULL && win->open) ? XT_FALSE : XT_TRUE;
295
+ }
296
+
297
+ /** `/driver`: which SDL_GPU backend the device is running (`"vulkan"`,
298
+ * `"direct3d12"`, `"metal"`, …). The engine asks SDL for the best available
299
+ * one, so this is how a program (or a test) confirms the path in use. */
300
+ static xt_value win_driver(xt_value self, xt_value env, int32_t argc, xt_value *argv) {
301
+ (void)self;
302
+ (void)env;
303
+ (void)argc;
304
+ (void)argv;
305
+ return xt_string_from_cstr(xt_gui_driver());
306
+ }
307
+
286
308
  /** `requestAnimationFrame(fn)` -> id. The callback runs once on the next frame
287
309
  * with the frame timestamp (ms) as its only argument. */
288
310
  static xt_value win_request_animation_frame(xt_value self, xt_value env, int32_t argc,
@@ -355,6 +377,22 @@ static xt_value win_off(xt_value self, xt_value env, int32_t argc, xt_value *arg
355
377
  return self;
356
378
  }
357
379
 
380
+ /** Web spelling of 'on': 'window.addEventListener(type, fn, options?)'. The
381
+ * optional 'options' argument ('capture'/'once') is accepted and ignored —
382
+ * window-level delivery has no capture phase — so a document written against
383
+ * the browser API compiles and behaves the same here. */
384
+ static xt_value win_add_event_listener(xt_value self, xt_value env, int32_t argc, xt_value *argv) {
385
+ win_on(self, env, argc, argv);
386
+ return XT_UNDEFINED;
387
+ }
388
+
389
+ /** Web spelling of 'off': 'window.removeEventListener(type, fn, options?)'. */
390
+ static xt_value win_remove_event_listener(xt_value self, xt_value env, int32_t argc,
391
+ xt_value *argv) {
392
+ win_off(self, env, argc, argv);
393
+ return XT_UNDEFINED;
394
+ }
395
+
358
396
  /* -- input diagnostics ---------------------------------------------------- */
359
397
 
360
398
  static double xt_gui_number_or(xt_value value, double fallback) {
@@ -455,10 +493,16 @@ xt_value xt_gui_window_proto(void) {
455
493
  define_method(proto, "fontMetrics", (void *)win_font_metrics);
456
494
  define_method(proto, "close", (void *)win_close);
457
495
  define_method(proto, "isOpen", (void *)win_is_open);
496
+ define_getter(proto, "closed", (void *)win_closed_get);
497
+ define_method(proto, "driver", (void *)win_driver);
458
498
  define_method(proto, "requestAnimationFrame", (void *)win_request_animation_frame);
459
499
  define_method(proto, "cancelAnimationFrame", (void *)win_cancel_animation_frame);
460
500
  define_method(proto, "on", (void *)win_on);
461
501
  define_method(proto, "off", (void *)win_off);
502
+ /* Web spellings, so a document written against the browser API runs
503
+ * unchanged under the engine. */
504
+ define_method(proto, "addEventListener", (void *)win_add_event_listener);
505
+ define_method(proto, "removeEventListener", (void *)win_remove_event_listener);
462
506
  define_method(proto, "hitTest", (void *)win_hit_test);
463
507
  define_method(proto, "sendEvent", (void *)win_send_event);
464
508
  g_window_proto = proto;
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
  }
@@ -38,6 +38,11 @@ xt_value xt_console_assert(int32_t argc, xt_value *argv) {
38
38
  static char *xt_console_labels[XT_CONSOLE_MAX_LABELS];
39
39
  static long xt_console_counts[XT_CONSOLE_MAX_LABELS];
40
40
  static clock_t xt_console_timers[XT_CONSOLE_MAX_LABELS];
41
+ /* A running timer is tracked by its own flag: clock() legitimately returns 0
42
+ (notably on Windows, where its resolution is one millisecond and a process
43
+ started less than a millisecond ago reports 0), so the timestamp itself
44
+ cannot serve as the "no timer running" sentinel. */
45
+ static unsigned char xt_console_timer_active[XT_CONSOLE_MAX_LABELS];
41
46
  static int xt_console_label_count = 0;
42
47
  static int xt_console_depth = 0;
43
48
 
@@ -51,6 +56,7 @@ static int xt_console_find_label(const char *label) {
51
56
  memcpy(xt_console_labels[index], label, strlen(label) + 1);
52
57
  xt_console_counts[index] = 0;
53
58
  xt_console_timers[index] = 0;
59
+ xt_console_timer_active[index] = 0;
54
60
  return index;
55
61
  }
56
62
 
@@ -96,14 +102,17 @@ xt_value xt_console_table(int32_t argc, xt_value *argv) {
96
102
  xt_value xt_console_time(int32_t argc, xt_value *argv) {
97
103
  const char *label = xt_console_label(argc, argv);
98
104
  int index = xt_console_find_label(label ? label : "default");
99
- if (index >= 0) xt_console_timers[index] = clock();
105
+ if (index >= 0) {
106
+ xt_console_timers[index] = clock();
107
+ xt_console_timer_active[index] = 1;
108
+ }
100
109
  return XT_UNDEFINED;
101
110
  }
102
111
 
103
112
  xt_value xt_console_time_log(int32_t argc, xt_value *argv) {
104
113
  const char *label = xt_console_label(argc, argv);
105
114
  int index = xt_console_find_label(label ? label : "default");
106
- if (index >= 0 && xt_console_timers[index] != 0) {
115
+ if (index >= 0 && xt_console_timer_active[index]) {
107
116
  double ms = (double)(clock() - xt_console_timers[index]) * 1000.0 / CLOCKS_PER_SEC;
108
117
  printf("%s: %.3fms\n", xt_console_labels[index], ms);
109
118
  }
@@ -114,6 +123,9 @@ xt_value xt_console_time_end(int32_t argc, xt_value *argv) {
114
123
  xt_console_time_log(argc, argv);
115
124
  const char *label = xt_console_label(argc, argv);
116
125
  int index = xt_console_find_label(label ? label : "default");
117
- if (index >= 0) xt_console_timers[index] = 0;
126
+ if (index >= 0) {
127
+ xt_console_timers[index] = 0;
128
+ xt_console_timer_active[index] = 0;
129
+ }
118
130
  return XT_UNDEFINED;
119
131
  }
@@ -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. */