xbintsc 0.3.79 → 0.3.81

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 (53) hide show
  1. package/README.md +1 -3
  2. package/README.zh-CN.md +1 -2
  3. package/dist/src/driver/compiler.js +6 -1
  4. package/dist/src/driver/compiler.js.map +1 -1
  5. package/dist/src/extensions/gui/index.js +10 -2
  6. package/dist/src/extensions/gui/index.js.map +1 -1
  7. package/dist/tests/e2e/gui-example-svg.test.d.ts +21 -0
  8. package/dist/tests/e2e/gui-example-svg.test.js +105 -0
  9. package/dist/tests/e2e/gui-example-svg.test.js.map +1 -0
  10. package/dist/tests/e2e/gui-transform.test.d.ts +7 -0
  11. package/dist/tests/e2e/gui-transform.test.js +198 -0
  12. package/dist/tests/e2e/gui-transform.test.js.map +1 -0
  13. package/doc/ai/cli.md +5 -0
  14. package/doc/ai/extensions.md +5 -1
  15. package/doc/ai/zh-CN/extensions.md +5 -1
  16. package/doc/gui/animation.md +53 -2
  17. package/doc/gui/api.md +1 -1
  18. package/doc/gui/dom.md +6 -1
  19. package/doc/gui/input.md +4 -1
  20. package/doc/gui/known-issues.md +16 -4
  21. package/doc/gui/layout.md +36 -0
  22. package/doc/gui/rendering.md +15 -0
  23. package/doc/gui.md +32 -3
  24. package/doc/implemented.md +1 -1
  25. package/doc/zh-CN/gui/animation.md +49 -2
  26. package/doc/zh-CN/gui/api.md +1 -1
  27. package/doc/zh-CN/gui/dom.md +6 -1
  28. package/doc/zh-CN/gui/input.md +2 -0
  29. package/doc/zh-CN/gui/known-issues.md +12 -2
  30. package/doc/zh-CN/gui/layout.md +31 -0
  31. package/doc/zh-CN/gui/rendering.md +11 -0
  32. package/doc/zh-CN/gui.md +28 -3
  33. package/doc/zh-CN/implemented.md +1 -1
  34. package/package.json +1 -1
  35. package/runtime/ext_gui/document.cpp +21 -3
  36. package/runtime/ext_gui/document.h +4 -1
  37. package/runtime/ext_gui/dom_api_node.cpp +6 -4
  38. package/runtime/ext_gui/layout.cpp +78 -1
  39. package/runtime/ext_gui/layout.h +21 -0
  40. package/runtime/ext_gui/paint.cpp +48 -15
  41. package/runtime/ext_gui/paint.h +8 -2
  42. package/runtime/ext_gui/renderer.cpp +4 -2
  43. package/runtime/ext_gui/renderer_image.cpp +2 -1
  44. package/runtime/ext_gui/renderer_internal.h +6 -1
  45. package/runtime/ext_gui/renderer_text.cpp +37 -15
  46. package/runtime/ext_gui/style.h +10 -0
  47. package/runtime/ext_gui/style_print.cpp +6 -0
  48. package/runtime/ext_gui/style_values.cpp +43 -0
  49. package/runtime/ext_gui/transform.cpp +412 -0
  50. package/runtime/ext_gui/transform.h +112 -0
  51. package/runtime/ext_gui/window.cpp +6 -4
  52. package/src/driver/compiler.ts +6 -1
  53. package/src/extensions/gui/index.ts +10 -2
@@ -0,0 +1,412 @@
1
+ /*
2
+ * xbintsc GUI engine — CSS `transform`/`transform-origin` parsing, composition
3
+ * and interpolation.
4
+ *
5
+ * `style.cpp`/`style_values.cpp` call `xt_transform_parse` to turn a declaration
6
+ * value into a list of `TransformOp`s; `layout.cpp` asks `xt_transform_matrix`
7
+ * for the composition once it knows the box size; `document.cpp` asks
8
+ * `xt_transform_interpolate` while a transition runs; `style_print.cpp` asks
9
+ * `xt_transform_to_string` for `getComputedStyle`.
10
+ */
11
+
12
+ #include "transform.h"
13
+
14
+ #include <cctype>
15
+ #include <cmath>
16
+ #include <cstdlib>
17
+
18
+ #include "css_internal.h"
19
+ #include "style_internal.h"
20
+
21
+ namespace xtgui {
22
+
23
+ namespace {
24
+
25
+ constexpr float kPi = 3.14159265358979323846f;
26
+ /** `rem` units inside a transform resolve against the initial font size; the
27
+ * computed style carries `font-size` in pixels, so the root size is not
28
+ * available here (layout only has the element's own style). */
29
+ constexpr float kRootFontSize = 16.0f;
30
+
31
+ bool isSpace(char c) { return xt_is_css_space(c); }
32
+
33
+ std::string trim(const std::string &text) { return xt_css_trim(text); }
34
+
35
+ /** Split `a, b` / `a b` / `a,b` into its arguments. */
36
+ std::vector<std::string> splitArgs(const std::string &text) {
37
+ std::vector<std::string> parts;
38
+ size_t index = 0;
39
+ while (index < text.size()) {
40
+ while (index < text.size() && (isSpace(text[index]) || text[index] == ',')) index++;
41
+ size_t start = index;
42
+ while (index < text.size() && !isSpace(text[index]) && text[index] != ',') index++;
43
+ if (index > start) parts.push_back(text.substr(start, index - start));
44
+ }
45
+ return parts;
46
+ }
47
+
48
+ bool parseLengthArg(const std::string &token, Length *out) {
49
+ Length length = xt_css_parse_length(token);
50
+ if (length.unit == Unit::Invalid || length.unit == Unit::Auto || length.unit == Unit::None) {
51
+ return false;
52
+ }
53
+ *out = length;
54
+ return true;
55
+ }
56
+
57
+ /** A unitless number, optionally a percentage (which is divided by 100). */
58
+ bool parseFactorArg(const std::string &raw, float *out) {
59
+ std::string token = trim(raw);
60
+ if (token.empty()) return false;
61
+ bool percent = token.back() == '%';
62
+ if (percent) token.pop_back();
63
+ float number = 0;
64
+ if (!xt_css_parse_number(token, &number)) return false;
65
+ *out = percent ? number / 100.0f : number;
66
+ return true;
67
+ }
68
+
69
+ bool parseAngleArg(const std::string &raw, float *out) {
70
+ std::string token = xt_css_lower(trim(raw));
71
+ if (token.empty()) return false;
72
+ char *end = nullptr;
73
+ double number = std::strtod(token.c_str(), &end);
74
+ if (end == token.c_str()) return false;
75
+ std::string unit = trim(std::string(end));
76
+ float radians = 0;
77
+ if (unit.empty() || unit == "deg") radians = (float)number * kPi / 180.0f;
78
+ else if (unit == "rad") radians = (float)number;
79
+ else if (unit == "grad") radians = (float)number * kPi / 200.0f;
80
+ else if (unit == "turn") radians = (float)number * 2.0f * kPi;
81
+ else return false;
82
+ *out = radians;
83
+ return true;
84
+ }
85
+
86
+ /** Translate one parsed function call into a `TransformOp`. */
87
+ bool applyFunction(const std::string &name, const std::vector<std::string> &args,
88
+ TransformOp *op) {
89
+ if (name == "translate" || name == "translate3d") {
90
+ /* The 3d spelling takes a `z` we parse and drop: the engine is 2D. */
91
+ size_t most = name == "translate3d" ? 3 : 2;
92
+ if (args.empty() || args.size() > most) return false;
93
+ op->kind = TransformKind::Translate;
94
+ if (!parseLengthArg(args[0], &op->x)) return false;
95
+ op->y = Length{0.0f, Unit::Px};
96
+ if (args.size() >= 2 && !parseLengthArg(args[1], &op->y)) return false;
97
+ return true;
98
+ }
99
+ if (name == "translatex") {
100
+ if (args.size() != 1) return false;
101
+ op->kind = TransformKind::TranslateX;
102
+ op->y = Length{0.0f, Unit::Px};
103
+ return parseLengthArg(args[0], &op->x);
104
+ }
105
+ if (name == "translatey") {
106
+ if (args.size() != 1) return false;
107
+ op->kind = TransformKind::TranslateY;
108
+ op->x = Length{0.0f, Unit::Px};
109
+ return parseLengthArg(args[0], &op->y);
110
+ }
111
+ if (name == "scale" || name == "scale3d") {
112
+ size_t most = name == "scale3d" ? 3 : 2;
113
+ if (args.empty() || args.size() > most) return false;
114
+ op->kind = TransformKind::Scale;
115
+ if (!parseFactorArg(args[0], &op->scale_x)) return false;
116
+ op->scale_y = op->scale_x;
117
+ if (args.size() >= 2 && !parseFactorArg(args[1], &op->scale_y)) return false;
118
+ return true;
119
+ }
120
+ if (name == "scalex") {
121
+ if (args.size() != 1) return false;
122
+ op->kind = TransformKind::ScaleX;
123
+ op->scale_y = 1.0f;
124
+ return parseFactorArg(args[0], &op->scale_x);
125
+ }
126
+ if (name == "scaley") {
127
+ if (args.size() != 1) return false;
128
+ op->kind = TransformKind::ScaleY;
129
+ op->scale_x = 1.0f;
130
+ return parseFactorArg(args[0], &op->scale_y);
131
+ }
132
+ if (name == "rotate" || name == "rotatez") {
133
+ if (args.size() != 1) return false;
134
+ op->kind = TransformKind::Rotate;
135
+ return parseAngleArg(args[0], &op->angle);
136
+ }
137
+ if (name == "matrix") {
138
+ if (args.size() != 6) return false;
139
+ op->kind = TransformKind::Matrix;
140
+ for (size_t i = 0; i < 6; i++) {
141
+ if (!xt_css_parse_number(args[i], &op->matrix[i])) return false;
142
+ }
143
+ return true;
144
+ }
145
+ return false; // skew/perspective/… are not supported yet
146
+ }
147
+
148
+ float resolveLength(const Length &length, float base, float em, float vw, float vh) {
149
+ return xt_style_resolve_absolute(length, base, em, kRootFontSize, vw, vh);
150
+ }
151
+
152
+ bool sameLength(const Length &left, const Length &right) {
153
+ return left.unit == right.unit && left.value == right.value;
154
+ }
155
+
156
+ bool sameOp(const TransformOp &left, const TransformOp &right) {
157
+ if (left.kind != right.kind) return false;
158
+ switch (left.kind) {
159
+ case TransformKind::Translate:
160
+ return sameLength(left.x, right.x) && sameLength(left.y, right.y);
161
+ case TransformKind::TranslateX:
162
+ return sameLength(left.x, right.x);
163
+ case TransformKind::TranslateY:
164
+ return sameLength(left.y, right.y);
165
+ case TransformKind::Scale:
166
+ return left.scale_x == right.scale_x && left.scale_y == right.scale_y;
167
+ case TransformKind::ScaleX:
168
+ return left.scale_x == right.scale_x;
169
+ case TransformKind::ScaleY:
170
+ return left.scale_y == right.scale_y;
171
+ case TransformKind::Rotate:
172
+ return left.angle == right.angle;
173
+ case TransformKind::Matrix:
174
+ for (int i = 0; i < 6; i++) {
175
+ if (left.matrix[i] != right.matrix[i]) return false;
176
+ }
177
+ return true;
178
+ }
179
+ return false;
180
+ }
181
+
182
+ bool mixLength(const Length &from, const Length &to, float t, Length *out) {
183
+ if (from.unit != to.unit) return false;
184
+ out->unit = from.unit;
185
+ out->value = from.value + (to.value - from.value) * t;
186
+ return true;
187
+ }
188
+
189
+ } // namespace
190
+
191
+ Matrix2D Matrix2D::rotate(float radians) {
192
+ Matrix2D out;
193
+ float cosine = std::cos(radians);
194
+ float sine = std::sin(radians);
195
+ out.a = cosine;
196
+ out.b = sine;
197
+ out.c = -sine;
198
+ out.d = cosine;
199
+ return out;
200
+ }
201
+
202
+ Matrix2D Matrix2D::multiply(const Matrix2D &other) const {
203
+ Matrix2D out;
204
+ out.a = a * other.a + c * other.b;
205
+ out.b = b * other.a + d * other.b;
206
+ out.c = a * other.c + c * other.d;
207
+ out.d = b * other.c + d * other.d;
208
+ out.e = a * other.e + c * other.f + e;
209
+ out.f = b * other.e + d * other.f + f;
210
+ return out;
211
+ }
212
+
213
+ bool Matrix2D::invert(Matrix2D *out) const {
214
+ float determinant = a * d - b * c;
215
+ if (std::fabs(determinant) < 1e-9f) return false;
216
+ float inverse = 1.0f / determinant;
217
+ Matrix2D result;
218
+ result.a = d * inverse;
219
+ result.b = -b * inverse;
220
+ result.c = -c * inverse;
221
+ result.d = a * inverse;
222
+ result.e = -(result.a * e + result.c * f);
223
+ result.f = -(result.b * e + result.d * f);
224
+ *out = result;
225
+ return true;
226
+ }
227
+
228
+ float Matrix2D::magnification() const {
229
+ float determinant = std::fabs(a * d - b * c);
230
+ return std::sqrt(determinant);
231
+ }
232
+
233
+ bool xt_transform_parse(const std::string &value, std::vector<TransformOp> *out) {
234
+ out->clear();
235
+ std::string text = trim(value);
236
+ std::string lowered = xt_css_lower(text);
237
+ if (lowered.empty() || lowered == "none") return true;
238
+
239
+ size_t pos = 0;
240
+ while (pos < text.size()) {
241
+ while (pos < text.size() && isSpace(text[pos])) pos++;
242
+ if (pos >= text.size()) break;
243
+ size_t name_start = pos;
244
+ /* A function name may carry digits: `translate3d`, `scale3d`, `matrix3d`. */
245
+ while (pos < text.size() && (std::isalnum((unsigned char)text[pos]) || text[pos] == '-')) pos++;
246
+ if (pos == name_start) return false;
247
+ std::string name = xt_css_lower(text.substr(name_start, pos - name_start));
248
+ while (pos < text.size() && isSpace(text[pos])) pos++;
249
+ if (pos >= text.size() || text[pos] != '(') return false;
250
+ pos++;
251
+ size_t args_start = pos;
252
+ int depth = 1;
253
+ while (pos < text.size() && depth > 0) {
254
+ if (text[pos] == '(') depth++;
255
+ else if (text[pos] == ')') depth--;
256
+ pos++;
257
+ }
258
+ if (depth != 0) return false;
259
+ std::string inner = text.substr(args_start, pos - 1 - args_start);
260
+ TransformOp op;
261
+ if (!applyFunction(name, splitArgs(inner), &op)) return false;
262
+ out->push_back(op);
263
+ }
264
+ return true;
265
+ }
266
+
267
+ bool xt_transform_equal(const std::vector<TransformOp> &left,
268
+ const std::vector<TransformOp> &right) {
269
+ if (left.size() != right.size()) return false;
270
+ for (size_t i = 0; i < left.size(); i++) {
271
+ if (!sameOp(left[i], right[i])) return false;
272
+ }
273
+ return true;
274
+ }
275
+
276
+ bool xt_transform_interpolate(const std::vector<TransformOp> &from,
277
+ const std::vector<TransformOp> &to, float t,
278
+ std::vector<TransformOp> *out) {
279
+ if (from.size() != to.size()) return false;
280
+ std::vector<TransformOp> mixed(from.size());
281
+ for (size_t i = 0; i < from.size(); i++) {
282
+ const TransformOp &left = from[i];
283
+ const TransformOp &right = to[i];
284
+ if (left.kind != right.kind) return false;
285
+ TransformOp op;
286
+ op.kind = left.kind;
287
+ switch (left.kind) {
288
+ case TransformKind::Translate:
289
+ if (!mixLength(left.x, right.x, t, &op.x)) return false;
290
+ if (!mixLength(left.y, right.y, t, &op.y)) return false;
291
+ break;
292
+ case TransformKind::TranslateX:
293
+ if (!mixLength(left.x, right.x, t, &op.x)) return false;
294
+ break;
295
+ case TransformKind::TranslateY:
296
+ if (!mixLength(left.y, right.y, t, &op.y)) return false;
297
+ break;
298
+ case TransformKind::Scale:
299
+ op.scale_x = left.scale_x + (right.scale_x - left.scale_x) * t;
300
+ op.scale_y = left.scale_y + (right.scale_y - left.scale_y) * t;
301
+ break;
302
+ case TransformKind::ScaleX:
303
+ op.scale_x = left.scale_x + (right.scale_x - left.scale_x) * t;
304
+ break;
305
+ case TransformKind::ScaleY:
306
+ op.scale_y = left.scale_y + (right.scale_y - left.scale_y) * t;
307
+ break;
308
+ case TransformKind::Rotate:
309
+ op.angle = left.angle + (right.angle - left.angle) * t;
310
+ break;
311
+ case TransformKind::Matrix:
312
+ for (int entry = 0; entry < 6; entry++) {
313
+ op.matrix[entry] = left.matrix[entry] + (right.matrix[entry] - left.matrix[entry]) * t;
314
+ }
315
+ break;
316
+ }
317
+ mixed[i] = op;
318
+ }
319
+ *out = mixed;
320
+ return true;
321
+ }
322
+
323
+ Matrix2D xt_transform_matrix(const std::vector<TransformOp> &ops, float percent_width,
324
+ float percent_height, float em, float viewport_width,
325
+ float viewport_height) {
326
+ Matrix2D result;
327
+ for (const TransformOp &op : ops) {
328
+ Matrix2D step;
329
+ switch (op.kind) {
330
+ case TransformKind::Translate:
331
+ step.e = resolveLength(op.x, percent_width, em, viewport_width, viewport_height);
332
+ step.f = resolveLength(op.y, percent_height, em, viewport_width, viewport_height);
333
+ break;
334
+ case TransformKind::TranslateX:
335
+ step.e = resolveLength(op.x, percent_width, em, viewport_width, viewport_height);
336
+ break;
337
+ case TransformKind::TranslateY:
338
+ step.f = resolveLength(op.y, percent_height, em, viewport_width, viewport_height);
339
+ break;
340
+ case TransformKind::Scale:
341
+ step = Matrix2D::scale(op.scale_x, op.scale_y);
342
+ break;
343
+ case TransformKind::ScaleX:
344
+ step = Matrix2D::scale(op.scale_x, 1.0f);
345
+ break;
346
+ case TransformKind::ScaleY:
347
+ step = Matrix2D::scale(1.0f, op.scale_y);
348
+ break;
349
+ case TransformKind::Rotate:
350
+ step = Matrix2D::rotate(op.angle);
351
+ break;
352
+ case TransformKind::Matrix:
353
+ step.a = op.matrix[0];
354
+ step.b = op.matrix[1];
355
+ step.c = op.matrix[2];
356
+ step.d = op.matrix[3];
357
+ step.e = op.matrix[4];
358
+ step.f = op.matrix[5];
359
+ break;
360
+ }
361
+ result = result.multiply(step);
362
+ }
363
+ return result;
364
+ }
365
+
366
+ std::string xt_transform_to_string(const std::vector<TransformOp> &ops) {
367
+ if (ops.empty()) return "none";
368
+ std::string text;
369
+ for (size_t i = 0; i < ops.size(); i++) {
370
+ if (i > 0) text += " ";
371
+ const TransformOp &op = ops[i];
372
+ switch (op.kind) {
373
+ case TransformKind::Translate:
374
+ text += "translate(" + xt_style_length_to_string(op.x) + ", " +
375
+ xt_style_length_to_string(op.y) + ")";
376
+ break;
377
+ case TransformKind::TranslateX:
378
+ text += "translateX(" + xt_style_length_to_string(op.x) + ")";
379
+ break;
380
+ case TransformKind::TranslateY:
381
+ text += "translateY(" + xt_style_length_to_string(op.y) + ")";
382
+ break;
383
+ case TransformKind::Scale:
384
+ text += "scale(" + xt_style_length_to_string(Length{op.scale_x, Unit::Number}) + ", " +
385
+ xt_style_length_to_string(Length{op.scale_y, Unit::Number}) + ")";
386
+ break;
387
+ case TransformKind::ScaleX:
388
+ text += "scaleX(" + xt_style_length_to_string(Length{op.scale_x, Unit::Number}) + ")";
389
+ break;
390
+ case TransformKind::ScaleY:
391
+ text += "scaleY(" + xt_style_length_to_string(Length{op.scale_y, Unit::Number}) + ")";
392
+ break;
393
+ case TransformKind::Rotate: {
394
+ char buffer[64];
395
+ std::snprintf(buffer, sizeof(buffer), "rotate(%.4gdeg)", (double)(op.angle * 180.0f / kPi));
396
+ text += buffer;
397
+ break;
398
+ }
399
+ case TransformKind::Matrix: {
400
+ char buffer[160];
401
+ std::snprintf(buffer, sizeof(buffer), "matrix(%.6g, %.6g, %.6g, %.6g, %.6g, %.6g)",
402
+ (double)op.matrix[0], (double)op.matrix[1], (double)op.matrix[2],
403
+ (double)op.matrix[3], (double)op.matrix[4], (double)op.matrix[5]);
404
+ text += buffer;
405
+ break;
406
+ }
407
+ }
408
+ }
409
+ return text;
410
+ }
411
+
412
+ } // namespace xtgui
@@ -0,0 +1,112 @@
1
+ /*
2
+ * xbintsc GUI engine — CSS `transform` and `transform-origin`.
3
+ *
4
+ * A `transform` declaration is parsed into a list of `TransformOp`s (the
5
+ * familiar `translate(...) rotate(...) scale(...)` chain, plus `matrix(...)`).
6
+ * Lengths stay unresolved in the list, because percentages and `em`/`vw`
7
+ * units need the box that carries them — which only layout knows. Layout
8
+ * therefore resolves the list into a `Matrix2D`, and paint, hit testing and
9
+ * `getBoundingClientRect` all consume that one matrix.
10
+ *
11
+ * Transforms are a *paint-time* effect: they never change layout geometry, so
12
+ * a transform cannot move a sibling, and it must be composed down the subtree
13
+ * (a child of a transformed box is transformed too).
14
+ */
15
+ #ifndef XT_GUI_TRANSFORM_H
16
+ #define XT_GUI_TRANSFORM_H
17
+
18
+ #include <string>
19
+ #include <vector>
20
+
21
+ #include "css.h"
22
+
23
+ namespace xtgui {
24
+
25
+ /** A 2D affine transform laid out like the DOM `DOMMatrix` (a b c d e f):
26
+ * `x' = a*x + c*y + e`, `y' = b*x + d*y + f`. */
27
+ struct Matrix2D {
28
+ float a = 1.0f, b = 0.0f, c = 0.0f, d = 1.0f, e = 0.0f, f = 0.0f;
29
+
30
+ bool identity() const {
31
+ return a == 1.0f && b == 0.0f && c == 0.0f && d == 1.0f && e == 0.0f && f == 0.0f;
32
+ }
33
+ /** True when the transform keeps axis-aligned rectangles axis aligned (no
34
+ * rotation or skew). */
35
+ bool axis_aligned() const { return b == 0.0f && c == 0.0f; }
36
+ void apply(float x, float y, float *out_x, float *out_y) const {
37
+ *out_x = a * x + c * y + e;
38
+ *out_y = b * x + d * y + f;
39
+ }
40
+ /** `this * other`: the point is put through `other` first, then `this`. */
41
+ Matrix2D multiply(const Matrix2D &other) const;
42
+ /** The inverse transform, or false for a singular matrix (scale 0). */
43
+ bool invert(Matrix2D *out) const;
44
+ /** Approximate uniform magnification (geometric mean of the axis scales). */
45
+ float magnification() const;
46
+
47
+ static Matrix2D translate(float x, float y) {
48
+ Matrix2D out;
49
+ out.e = x;
50
+ out.f = y;
51
+ return out;
52
+ }
53
+ static Matrix2D scale(float x, float y) {
54
+ Matrix2D out;
55
+ out.a = x;
56
+ out.d = y;
57
+ return out;
58
+ }
59
+ static Matrix2D rotate(float radians);
60
+ };
61
+
62
+ enum class TransformKind {
63
+ Translate,
64
+ TranslateX,
65
+ TranslateY,
66
+ Scale,
67
+ ScaleX,
68
+ ScaleY,
69
+ Rotate,
70
+ Matrix,
71
+ };
72
+
73
+ /** One function in a parsed `transform` list. Only the fields its `kind` uses
74
+ * carry a value; the rest stay at their neutral defaults. */
75
+ struct TransformOp {
76
+ TransformKind kind = TransformKind::Translate;
77
+ Length x; // translate x; `%` resolves against the box width
78
+ Length y; // translate y; `%` resolves against the box height
79
+ float scale_x = 1.0f; // scale x (a unitless factor)
80
+ float scale_y = 1.0f; // scale y
81
+ float angle = 0.0f; // rotate, in radians
82
+ float matrix[6] = {1, 0, 0, 1, 0, 0}; // a b c d e f
83
+ };
84
+
85
+ /** Parse a `transform` value. `none` (or an empty list) yields an empty vector.
86
+ * An unsupported function or a malformed argument returns false, which makes
87
+ * the whole declaration invalid — exactly what CSS does. */
88
+ bool xt_transform_parse(const std::string &value, std::vector<TransformOp> *out);
89
+
90
+ /** Structural equality (used to decide whether a transition is needed). */
91
+ bool xt_transform_equal(const std::vector<TransformOp> &left,
92
+ const std::vector<TransformOp> &right);
93
+
94
+ /** Interpolate two lists of the same shape (same kinds, compatible units) at
95
+ * progress `t` in [0, 1]. Returns false when the lists cannot be mixed, in
96
+ * which case the caller should snap to the target. */
97
+ bool xt_transform_interpolate(const std::vector<TransformOp> &from,
98
+ const std::vector<TransformOp> &to, float t,
99
+ std::vector<TransformOp> *out);
100
+
101
+ /** Compose a `transform` list into one matrix. `percent_width`/`percent_height`
102
+ * are the element's border-box size (the base for `%`), `em` its font size. */
103
+ Matrix2D xt_transform_matrix(const std::vector<TransformOp> &ops, float percent_width,
104
+ float percent_height, float em, float viewport_width,
105
+ float viewport_height);
106
+
107
+ /** Canonical function-list spelling, for `getComputedStyle(el, "transform")`. */
108
+ std::string xt_transform_to_string(const std::vector<TransformOp> &ops);
109
+
110
+ } // namespace xtgui
111
+
112
+ #endif /* XT_GUI_TRANSFORM_H */
@@ -147,11 +147,13 @@ static xt_value win_get_bounding_rect(xt_value self, xt_value env, int32_t argc,
147
147
  if (node == nullptr) return XT_UNDEFINED;
148
148
  const xtgui::LayoutBox *box = win->document->boxOf(node);
149
149
  if (box == nullptr) return XT_UNDEFINED;
150
+ /* Like the DOM: the rectangle of the *transformed* border box. */
151
+ xtgui::LayoutRect rect = xtgui::xt_layout_transformed_bounds(box);
150
152
  xt_value object = xt_object_new();
151
- xt_object_set(object, xt_string_from_cstr("x"), xt_number(box->x));
152
- xt_object_set(object, xt_string_from_cstr("y"), xt_number(box->y));
153
- xt_object_set(object, xt_string_from_cstr("width"), xt_number(box->width));
154
- xt_object_set(object, xt_string_from_cstr("height"), xt_number(box->height));
153
+ xt_object_set(object, xt_string_from_cstr("x"), xt_number(rect.x));
154
+ xt_object_set(object, xt_string_from_cstr("y"), xt_number(rect.y));
155
+ xt_object_set(object, xt_string_from_cstr("width"), xt_number(rect.width));
156
+ xt_object_set(object, xt_string_from_cstr("height"), xt_number(rect.height));
155
157
  return object;
156
158
  }
157
159
 
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
11
- import { basename, extname, join, resolve } from "node:path";
11
+ import { basename, dirname, extname, join, resolve } from "node:path";
12
12
  import { DiagnosticBag, DiagnosticCode, type Diagnostic } from "../diagnostics/diagnostic.js";
13
13
  import { SourceFile } from "../diagnostics/source.js";
14
14
  import { sourceTextBytes } from "../diagnostics/source-text.js";
@@ -261,6 +261,11 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
261
261
  }
262
262
 
263
263
  mkdirSync(outDir, { recursive: true });
264
+ /* An explicit `-o path/to/app.exe` (or `output` in the config) may name a
265
+ * directory that does not exist yet: the IR and object files land in `outDir`,
266
+ * but clang writes the *executable* to `outputPath`, and a missing parent
267
+ * directory there makes the link fail with `LNK1104: cannot open file`. */
268
+ mkdirSync(dirname(outputPath), { recursive: true });
264
269
  const irPath = resolve(outDir, basename(absoluteEntry, extname(absoluteEntry)) + ".ll");
265
270
  writeFileSync(irPath, ir);
266
271
 
@@ -98,8 +98,16 @@ export const guiExtension: Extension = {
98
98
  "-luser32", "-lgdi32", "-lshell32",
99
99
  "-lole32", "-loleaut32", "-luuid", "-ladvapi32",
100
100
  "-lwinmm", "-limm32", "-lversion", "-lsetupapi",
101
- /* The engine is C++; link the MSVC C++ runtime. */
102
- "-lmsvcprt",
101
+ /* The engine is C++, but `msvcprt` (the DLL C++ runtime) must not be
102
+ * named here: `gui.lib` is built with `CMAKE_MSVC_RUNTIME_LIBRARY=
103
+ * MultiThreaded` and every engine object records `libcpmt` (the static
104
+ * C++ runtime) as a dependent library — see `scripts/build-gui.ts`.
105
+ * The two runtimes define the same symbols, so requesting the DLL one
106
+ * as well fails the link with `LNK2038: mismatch detected for
107
+ * 'RuntimeLibrary'` plus a wall of `LNK2005: already defined` errors
108
+ * (surfacing as `clang: linker command failed with exit code 1169`).
109
+ * clang links the static `libcpmt` by default, which is the CRT the
110
+ * archive and the xbintsc runtime objects were both built against. */
103
111
  ];
104
112
  }
105
113
  return ["-lX11", "-lwayland-client", "-lEGL", "-lGL", "-ldl", "-lpthread", "-lstdc++"];