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.
- 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-events.test.js +32 -0
- package/dist/tests/e2e/gui-events.test.js.map +1 -1
- package/dist/tests/e2e/gui-example-browser.test.d.ts +12 -0
- package/dist/tests/e2e/gui-example-browser.test.js +168 -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 +120 -9
- package/doc/xbintsc.config.schema.json +4 -0
- package/doc/zh-CN/gui.md +78 -7
- 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 +13 -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 +44 -0
- package/runtime/rt.h +10 -0
- package/runtime/xt_io/console.inc +15 -3
- package/src/driver/compiler.ts +26 -1
- package/src/driver/config.ts +7 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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,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)
|
|
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 &&
|
|
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)
|
|
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
|
}
|
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. */
|