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.
- package/dist/src/driver/compiler.js +25 -1
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/driver/config.d.ts +7 -0
- package/dist/src/driver/config.js.map +1 -1
- package/dist/src/driver/win-entry.d.ts +21 -0
- package/dist/src/driver/win-entry.js +52 -0
- package/dist/src/driver/win-entry.js.map +1 -0
- package/dist/tests/e2e/gui-example-browser.test.d.ts +12 -0
- package/dist/tests/e2e/gui-example-browser.test.js +164 -0
- package/dist/tests/e2e/gui-example-browser.test.js.map +1 -0
- package/dist/tests/e2e/gui-example.test.d.ts +14 -0
- package/dist/tests/e2e/gui-example.test.js +76 -0
- package/dist/tests/e2e/gui-example.test.js.map +1 -0
- package/dist/tests/e2e/gui-helpers.d.ts +39 -4
- package/dist/tests/e2e/gui-helpers.js +38 -9
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-layout.test.js +155 -0
- package/dist/tests/e2e/gui-layout.test.js.map +1 -1
- package/dist/tests/e2e/gui-lifecycle.test.js +10 -1
- package/dist/tests/e2e/gui-lifecycle.test.js.map +1 -1
- package/dist/tests/e2e/gui-paint.test.js +44 -4
- package/dist/tests/e2e/gui-paint.test.js.map +1 -1
- package/dist/tests/e2e/gui-script.test.js +8 -4
- package/dist/tests/e2e/gui-script.test.js.map +1 -1
- package/doc/gui.md +113 -7
- package/doc/xbintsc.config.schema.json +4 -0
- package/doc/zh-CN/gui.md +74 -6
- package/package.json +1 -1
- package/runtime/ext_gui/dom_api.cpp +19 -0
- package/runtime/ext_gui/dom_api_node.cpp +4 -1
- package/runtime/ext_gui/dom_api_node_read.cpp +12 -0
- package/runtime/ext_gui/gui.cpp +42 -2
- package/runtime/ext_gui/gui_engine.h +15 -0
- package/runtime/ext_gui/gui_events.cpp +10 -3
- package/runtime/ext_gui/layout.cpp +6 -0
- package/runtime/ext_gui/layout.h +4 -0
- package/runtime/ext_gui/layout_flex.cpp +13 -5
- package/runtime/ext_gui/layout_flow.cpp +134 -4
- package/runtime/ext_gui/layout_flow_internal.h +41 -2
- package/runtime/ext_gui/layout_inline.cpp +11 -3
- package/runtime/ext_gui/paint.cpp +6 -0
- package/runtime/ext_gui/script.cpp +12 -2
- package/runtime/ext_gui/text.cpp +14 -0
- package/runtime/ext_gui/window.cpp +12 -0
- package/runtime/rt.h +10 -0
- package/src/driver/compiler.ts +26 -1
- package/src/driver/config.ts +7 -0
- 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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
19
|
-
* runtime
|
|
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;
|
package/runtime/ext_gui/text.cpp
CHANGED
|
@@ -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
|
}
|
package/src/driver/compiler.ts
CHANGED
|
@@ -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: [
|
|
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,
|
package/src/driver/config.ts
CHANGED
|
@@ -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
|
+
}
|