xbintsc 0.3.69 → 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 (115) hide show
  1. package/README.md +57 -4
  2. package/README.zh-CN.md +51 -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-advanced.test.d.ts +6 -0
  8. package/dist/tests/e2e/gui-advanced.test.js +261 -0
  9. package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
  10. package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
  11. package/dist/tests/e2e/gui-canvas.test.js +233 -0
  12. package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
  13. package/dist/tests/e2e/gui-example-svg.test.d.ts +21 -0
  14. package/dist/tests/e2e/gui-example-svg.test.js +105 -0
  15. package/dist/tests/e2e/gui-example-svg.test.js.map +1 -0
  16. package/dist/tests/e2e/gui-example.test.d.ts +1 -1
  17. package/dist/tests/e2e/gui-example.test.js +2 -2
  18. package/dist/tests/e2e/gui-helpers.d.ts +3 -0
  19. package/dist/tests/e2e/gui-helpers.js +5 -0
  20. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  21. package/dist/tests/e2e/gui-layout.test.js +73 -0
  22. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  23. package/dist/tests/e2e/gui-media.test.d.ts +8 -0
  24. package/dist/tests/e2e/gui-media.test.js +119 -0
  25. package/dist/tests/e2e/gui-media.test.js.map +1 -0
  26. package/dist/tests/e2e/gui-paint.test.js +40 -0
  27. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  28. package/dist/tests/e2e/gui-transform.test.d.ts +7 -0
  29. package/dist/tests/e2e/gui-transform.test.js +198 -0
  30. package/dist/tests/e2e/gui-transform.test.js.map +1 -0
  31. package/doc/ai/cli.md +5 -0
  32. package/doc/ai/extensions.md +20 -3
  33. package/doc/ai/zh-CN/extensions.md +14 -3
  34. package/doc/gui/animation.md +97 -0
  35. package/doc/gui/api.md +92 -0
  36. package/doc/gui/build.md +91 -0
  37. package/doc/gui/canvas.md +98 -0
  38. package/doc/gui/dom.md +128 -0
  39. package/doc/gui/input.md +25 -0
  40. package/doc/gui/known-issues.md +78 -0
  41. package/doc/gui/layout.md +108 -0
  42. package/doc/gui/media.md +87 -0
  43. package/doc/gui/rendering.md +113 -0
  44. package/doc/gui/runtime.md +88 -0
  45. package/doc/gui-scripts.md +7 -3
  46. package/doc/gui.md +109 -612
  47. package/doc/implemented.md +1 -0
  48. package/doc/zh-CN/gui/animation.md +93 -0
  49. package/doc/zh-CN/gui/api.md +88 -0
  50. package/doc/zh-CN/gui/build.md +84 -0
  51. package/doc/zh-CN/gui/canvas.md +80 -0
  52. package/doc/zh-CN/gui/dom.md +119 -0
  53. package/doc/zh-CN/gui/input.md +23 -0
  54. package/doc/zh-CN/gui/known-issues.md +60 -0
  55. package/doc/zh-CN/gui/layout.md +91 -0
  56. package/doc/zh-CN/gui/media.md +71 -0
  57. package/doc/zh-CN/gui/rendering.md +92 -0
  58. package/doc/zh-CN/gui/runtime.md +76 -0
  59. package/doc/zh-CN/gui-scripts.md +6 -2
  60. package/doc/zh-CN/gui.md +89 -532
  61. package/doc/zh-CN/implemented.md +1 -0
  62. package/llms.txt +15 -2
  63. package/package.json +1 -1
  64. package/runtime/ext_gui/canvas.cpp +580 -0
  65. package/runtime/ext_gui/canvas.h +170 -0
  66. package/runtime/ext_gui/canvas_internal.h +59 -0
  67. package/runtime/ext_gui/canvas_raster.cpp +254 -0
  68. package/runtime/ext_gui/document.cpp +45 -4
  69. package/runtime/ext_gui/document.h +15 -1
  70. package/runtime/ext_gui/dom_api.cpp +3 -0
  71. package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
  72. package/runtime/ext_gui/dom_api_internal.h +73 -0
  73. package/runtime/ext_gui/dom_api_media.cpp +274 -0
  74. package/runtime/ext_gui/dom_api_node.cpp +6 -4
  75. package/runtime/ext_gui/dom_api_proto.cpp +80 -0
  76. package/runtime/ext_gui/gui.cpp +6 -0
  77. package/runtime/ext_gui/gui_canvas.cpp +95 -0
  78. package/runtime/ext_gui/gui_engine.h +49 -0
  79. package/runtime/ext_gui/gui_events.cpp +21 -5
  80. package/runtime/ext_gui/gui_media.cpp +193 -0
  81. package/runtime/ext_gui/image.cpp +31 -12
  82. package/runtime/ext_gui/image.h +20 -7
  83. package/runtime/ext_gui/layout.cpp +130 -6
  84. package/runtime/ext_gui/layout.h +54 -1
  85. package/runtime/ext_gui/layout_flex.cpp +263 -99
  86. package/runtime/ext_gui/layout_flow.cpp +85 -9
  87. package/runtime/ext_gui/layout_flow_internal.h +84 -5
  88. package/runtime/ext_gui/layout_inline.cpp +54 -37
  89. package/runtime/ext_gui/media.cpp +514 -0
  90. package/runtime/ext_gui/media.h +104 -0
  91. package/runtime/ext_gui/nanosvg.h +3278 -0
  92. package/runtime/ext_gui/nanosvgrast.h +1472 -0
  93. package/runtime/ext_gui/paint.cpp +156 -38
  94. package/runtime/ext_gui/paint.h +42 -6
  95. package/runtime/ext_gui/renderer.cpp +117 -12
  96. package/runtime/ext_gui/renderer.h +49 -9
  97. package/runtime/ext_gui/renderer_image.cpp +205 -78
  98. package/runtime/ext_gui/renderer_internal.h +12 -9
  99. package/runtime/ext_gui/renderer_text.cpp +46 -39
  100. package/runtime/ext_gui/style.h +21 -0
  101. package/runtime/ext_gui/style_print.cpp +17 -0
  102. package/runtime/ext_gui/style_ua.cpp +7 -0
  103. package/runtime/ext_gui/style_values.cpp +83 -1
  104. package/runtime/ext_gui/svg.cpp +182 -0
  105. package/runtime/ext_gui/svg.h +46 -0
  106. package/runtime/ext_gui/text.cpp +156 -25
  107. package/runtime/ext_gui/text.h +11 -2
  108. package/runtime/ext_gui/transform.cpp +412 -0
  109. package/runtime/ext_gui/transform.h +112 -0
  110. package/runtime/ext_gui/window.cpp +10 -5
  111. package/scripts/build-gui.ts +148 -6
  112. package/scripts/check-file-length.ts +5 -1
  113. package/scripts/install-gui-deps.sh +28 -0
  114. package/src/driver/compiler.ts +6 -1
  115. package/src/extensions/gui/index.ts +10 -2
@@ -0,0 +1,119 @@
1
+ /**
2
+ * End-to-end tests for `<video>` / `<audio>` support in the `gui` extension.
3
+ *
4
+ * These exercise the real decoding path (vendored FFmpeg + SDL audio) and the
5
+ * DOM media API. The fixtures live in `examples/resources`; when they are
6
+ * absent (a trim checkout) the suite is skipped rather than failing.
7
+ */
8
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { dirname, join, resolve } from "node:path";
11
+ import { spawnSync } from "node:child_process";
12
+ import { fileURLToPath } from "node:url";
13
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
14
+ import { build } from "../../src/driver/compiler.js";
15
+ import { createDefaultRegistry } from "../../src/extensions/registry.js";
16
+ import { guiExtension } from "../../src/extensions/gui/index.js";
17
+ import { createGuiHarness, guiAvailable } from "./gui-helpers.js";
18
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
19
+ const resourceDir = join(root, "examples", "resources");
20
+ const moviePath = join(resourceDir, "movie.mp4");
21
+ const soundPath = join(resourceDir, "sounds.mp3");
22
+ const exampleDir = join(root, "examples", "gui");
23
+ /** A local path safe to embed in an HTML attribute (no percent-encoding: the
24
+ * media loader hands the string straight to FFmpeg). */
25
+ function mediaSrc(absolutePath) {
26
+ return absolutePath.split("\\").join("/");
27
+ }
28
+ const resourcesAvailable = existsSync(moviePath) && existsSync(soundPath);
29
+ describe.skipIf(!guiAvailable || !resourcesAvailable)("gui extension — media", () => {
30
+ const harness = createGuiHarness();
31
+ beforeAll(() => harness.setup());
32
+ afterAll(() => harness.cleanup());
33
+ it("decodes video/audio and drives playback through the DOM API", () => {
34
+ const movie = mediaSrc(moviePath);
35
+ const sound = mediaSrc(soundPath);
36
+ const { value, stdout } = harness.compileAndRun("media", `
37
+ import { createWindow, run } from "gui";
38
+
39
+ const win = createWindow({ title: "media", width: 400, height: 300 });
40
+ win.on("ready", () => {
41
+ win.loadHTML(\`<html><body style="margin:0">
42
+ <video id="v" src="${movie}" autoplay muted></video>
43
+ <audio id="a" src="${sound}"></audio>
44
+ </body></html>\`);
45
+
46
+ const v: any = win.document.getElementById("v");
47
+ const a: any = win.document.getElementById("a");
48
+ let ticks = 0;
49
+
50
+ v.addEventListener("loadedmetadata", () => {
51
+ console.log("duration=" + (v.duration > 0) + ":" + (a.duration > 0));
52
+ console.log("dims=" + (v.videoWidth > 0) + ":" + (v.videoHeight > 0));
53
+ console.log("ready=" + v.readyState);
54
+ v.pause();
55
+ console.log("paused=" + v.paused);
56
+ v.currentTime = 2.5;
57
+ console.log("seek=" + (v.currentTime >= 2.4));
58
+ v.volume = 0.25;
59
+ console.log("volume=" + v.volume);
60
+ v.muted = true;
61
+ console.log("muted=" + v.muted);
62
+ v.play();
63
+ console.log("playing=" + (v.paused === false));
64
+ });
65
+ v.addEventListener("error", () => console.log("video-error"));
66
+ a.addEventListener("error", () => console.log("audio-error"));
67
+ v.addEventListener("timeupdate", () => {
68
+ ticks = ticks + 1;
69
+ if (ticks >= 2) {
70
+ console.log("advanced=" + (v.currentTime > 2.4));
71
+ win.close();
72
+ }
73
+ });
74
+ });
75
+
76
+ run();
77
+ `, { autocloseMs: 8000 });
78
+ expect(stdout).not.toContain("video-error");
79
+ expect(stdout).not.toContain("audio-error");
80
+ expect(value("duration")).toBe("true:true");
81
+ expect(value("dims")).toBe("true:true");
82
+ expect(value("paused")).toBe("true");
83
+ expect(value("seek")).toBe("true");
84
+ expect(value("volume")).toBe("0.25");
85
+ expect(value("muted")).toBe("true");
86
+ expect(value("playing")).toBe("true");
87
+ expect(value("advanced")).toBe("true");
88
+ });
89
+ it("runs the media example from the repository root", () => {
90
+ const workdir = mkdtempSync(join(tmpdir(), "xbintsc-media-"));
91
+ try {
92
+ writeFileSync(join(workdir, "media.html"), readFileSync(join(exampleDir, "media.html"), "utf8"));
93
+ writeFileSync(join(workdir, "media.ts"), readFileSync(join(exampleDir, "media.ts"), "utf8"));
94
+ const extensions = createDefaultRegistry().register(guiExtension);
95
+ const result = build(join(workdir, "media.ts"), {
96
+ emit: "exe",
97
+ outDir: join(workdir, "out"),
98
+ cacheDir: join(workdir, ".cache"),
99
+ extensions,
100
+ });
101
+ expect(result.diagnostics.filter((diagnostic) => diagnostic.category === "error")).toEqual([]);
102
+ // The example's `src` paths are relative to the working directory.
103
+ const executed = spawnSync(result.outputPath, [], {
104
+ cwd: root,
105
+ encoding: "utf8",
106
+ env: { ...process.env, XT_GUI_AUTOCLOSE_MS: "8000" },
107
+ });
108
+ expect(executed.status, executed.stderr).toBe(0);
109
+ expect(executed.stdout).toMatch(/video = \d+x\d+ duration=\d/);
110
+ expect(executed.stdout).toMatch(/audio duration=\d/);
111
+ expect(executed.stdout).toContain("play paused=false");
112
+ expect(executed.stdout).toContain("closed");
113
+ }
114
+ finally {
115
+ rmSync(workdir, { recursive: true, force: true });
116
+ }
117
+ });
118
+ });
119
+ //# sourceMappingURL=gui-media.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gui-media.test.js","sourceRoot":"","sources":["../../../tests/e2e/gui-media.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACvF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAElE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAC1E,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;AACxD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AACjD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;AAClD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;AAEjD;wDACwD;AACxD,SAAS,QAAQ,CAAC,YAAoB;IACpC,OAAO,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,kBAAkB,GAAG,UAAU,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC;AAE1E,QAAQ,CAAC,MAAM,CAAC,CAAC,YAAY,IAAI,CAAC,kBAAkB,CAAC,CAAC,uBAAuB,EAAE,GAAG,EAAE;IAClF,MAAM,OAAO,GAAG,gBAAgB,EAAE,CAAC;IACnC,SAAS,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAElC,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;QAClC,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;QAClC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,CAC7C,OAAO,EACP;;;;;;+BAMyB,KAAK;+BACL,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkC7B,EACD,EAAE,WAAW,EAAE,IAAI,EAAE,CACtB,CAAC;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QAC5C,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC5C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACxC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC;YACH,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;YACjG,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;YAC7F,MAAM,UAAU,GAAG,qBAAqB,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YAClE,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE;gBAC9C,IAAI,EAAE,KAAK;gBACX,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC;gBAC5B,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC;gBACjC,UAAU;aACX,CAAC,CAAC;YACH,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAC/F,mEAAmE;YACnE,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,EAAE;gBAChD,GAAG,EAAE,IAAI;gBACT,QAAQ,EAAE,MAAM;gBAChB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,mBAAmB,EAAE,MAAM,EAAE;aACrD,CAAC,CAAC;YACH,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjD,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAC;YAC/D,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;YACrD,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;YACvD,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC9C,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -41,6 +41,46 @@ describe.skipIf(!guiAvailable)("gui extension — paint", () => {
41
41
  expect(stdout).toContain(`image x=0.0 y=0.0 w=40.0 h=40.0 src=${src}`);
42
42
  expect(stdout).toContain(`image x=0.0 y=40.0 w=4.0 h=4.0 src=${src}`);
43
43
  });
44
+ it("sizes and paints an SVG (intrinsic and explicit)", () => {
45
+ // An SVG is a vector source: the image loader detects it and rasterises it
46
+ // at the drawn size instead of failing like `stb_image` alone would.
47
+ const src = harness.writeAsset("logo.svg", `<svg xmlns="http://www.w3.org/2000/svg" width="8" height="6" viewBox="0 0 8 6">` +
48
+ `<rect width="8" height="6" fill="#ff0000"/>` +
49
+ `<circle cx="4" cy="3" r="2" fill="#0000ff"/></svg>`);
50
+ const { stdout, stderr } = harness.compileAndRun("svg", `
51
+ import { createWindow, run } from "gui";
52
+
53
+ const HTML = \`
54
+ <html><head><style>
55
+ body { margin: 0; }
56
+ #scaled { width: 64px; height: 48px; }
57
+ </style></head><body>
58
+ <img id="natural" src="${src}">
59
+ <img id="scaled" src="${src}">
60
+ </body></html>
61
+ \`;
62
+
63
+ const win = createWindow({ title: "svg", width: 400, height: 300 });
64
+ win.on("ready", () => {
65
+ const natural = win.getBoundingClientRect("#natural");
66
+ console.log("natural=" + natural.width + "x" + natural.height);
67
+ const scaled = win.getBoundingClientRect("#scaled");
68
+ console.log("scaled=" + scaled.width + "x" + scaled.height);
69
+ console.log(win.paintList().trim());
70
+ win.close();
71
+ });
72
+ win.loadHTML(HTML);
73
+ run();
74
+ `);
75
+ // Intrinsic size comes from the SVG's width/height; the explicit CSS size
76
+ // wins for the second image.
77
+ expect(stdout).toContain("natural=8x6");
78
+ expect(stdout).toContain("scaled=64x48");
79
+ expect(stdout).toContain(`image x=0.0 y=0.0 w=8.0 h=6.0 src=${src}`);
80
+ expect(stdout).toContain(`image x=0.0 y=6.0 w=64.0 h=48.0 src=${src}`);
81
+ // The SVG must actually rasterise, not just lay out.
82
+ expect(stderr).not.toContain("could not decode image");
83
+ });
44
84
  it("builds a display list of backgrounds and borders", () => {
45
85
  const { value, stdout } = harness.compileAndRun("paint", `
46
86
  import { createWindow, run } from "gui";
@@ -1 +1 @@
1
- {"version":3,"file":"gui-paint.test.js","sourceRoot":"","sources":["../../../tests/e2e/gui-paint.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gui-paint.test.js","sourceRoot":"","sources":["../../../tests/e2e/gui-paint.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * End-to-end tests for the `gui` extension — CSS `transform` and
3
+ * `transform-origin`: parsing/computed style, the matrix the paint list carries,
4
+ * transformed geometry (`getBoundingClientRect`), hit testing, and animating a
5
+ * transform through a CSS transition.
6
+ */
7
+ export {};
@@ -0,0 +1,198 @@
1
+ /**
2
+ * End-to-end tests for the `gui` extension — CSS `transform` and
3
+ * `transform-origin`: parsing/computed style, the matrix the paint list carries,
4
+ * transformed geometry (`getBoundingClientRect`), hit testing, and animating a
5
+ * transform through a CSS transition.
6
+ */
7
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
8
+ import { createGuiHarness, guiAvailable } from "./gui-helpers.js";
9
+ const harness = createGuiHarness();
10
+ describe.skipIf(!guiAvailable)("gui extension — transform", () => {
11
+ beforeAll(() => harness.setup());
12
+ afterAll(() => harness.cleanup());
13
+ it("composes translate/rotate/scale into one matrix", () => {
14
+ const { stdout, value } = harness.compileAndRun("transform-matrix", `
15
+ import { createWindow, run } from "gui";
16
+
17
+ const HTML = \`
18
+ <html><head><style>
19
+ body { margin: 0; }
20
+ #moved {
21
+ position: absolute; left: 100px; top: 100px; width: 100px; height: 50px;
22
+ background-color: #ff0000;
23
+ transform-origin: 0 0;
24
+ transform: rotate(90deg) translate(10px, 0px);
25
+ }
26
+ #grew {
27
+ position: absolute; left: 100px; top: 200px; width: 40px; height: 20px;
28
+ background-color: #00ff00;
29
+ transform: scale(2, 3);
30
+ }
31
+ </style></head><body>
32
+ <div id="moved"></div><div id="grew"></div>
33
+ </body></html>
34
+ \`;
35
+
36
+ const win = createWindow({ title: "transform", width: 400, height: 300 });
37
+ win.on("ready", () => {
38
+ console.log("computed=" + win.computedStyle("#moved", "transform"));
39
+ console.log("origin=" + win.computedStyle("#moved", "transform-origin"));
40
+ console.log("default-origin=" + win.computedStyle("#grew", "transform-origin"));
41
+ const moved = win.getBoundingClientRect("#moved");
42
+ console.log("moved=" + moved.x.toFixed(1) + "," + moved.y.toFixed(1) + "," +
43
+ moved.width.toFixed(1) + "," + moved.height.toFixed(1));
44
+ const grew = win.getBoundingClientRect("#grew");
45
+ console.log("grew=" + grew.x.toFixed(1) + "," + grew.y.toFixed(1) + "," +
46
+ grew.width.toFixed(1) + "," + grew.height.toFixed(1));
47
+ console.log(win.paintList().trim());
48
+ win.close();
49
+ });
50
+ win.loadHTML(HTML);
51
+ run();
52
+ `);
53
+ expect(value("computed")).toBe("rotate(90deg) translate(10px, 0px)");
54
+ expect(value("origin")).toBe("0px 0px");
55
+ // `transform-origin` defaults to the centre of the border box.
56
+ expect(value("default-origin")).toBe("50% 50%");
57
+ /* rotate(90deg) about the top-left corner turns the 100x50 box into a
58
+ * 50x100 one; the local `translate(10px, 0)` moves it 10px down. */
59
+ expect(value("moved")).toBe("50.0,110.0,50.0,100.0");
60
+ // `scale(2, 3)` about the centre: 40x20 -> 80x60, centred on the same point.
61
+ expect(value("grew")).toBe("80.0,180.0,80.0,60.0");
62
+ // Paint keeps the untransformed layout geometry and carries the matrix.
63
+ expect(stdout).toContain("rect x=100.0 y=100.0 w=100.0 h=50.0 r=0.0 color=#ff0000ff transform=matrix(");
64
+ expect(stdout).toContain("transform=matrix(2,0,0,3,-120,-420)");
65
+ });
66
+ it("parses the 3d spellings and drops unsupported functions", () => {
67
+ const { stdout, value } = harness.compileAndRun("transform-parse", `
68
+ import { createWindow, run } from "gui";
69
+
70
+ const HTML = \`
71
+ <html><head><style>
72
+ body { margin: 0; }
73
+ #a {
74
+ position: absolute; left: 0px; top: 0px; width: 40px; height: 20px;
75
+ background-color: #ff0000; transform-origin: 0 0;
76
+ transform: translate3d(10px, 4px, 0) scale3d(2, 3, 1) rotateZ(0.5turn);
77
+ }
78
+ #b {
79
+ position: absolute; left: 0px; top: 100px; width: 40px; height: 20px;
80
+ background-color: #00ff00; transform: skewX(10deg);
81
+ }
82
+ #c {
83
+ position: absolute; left: 100px; top: 100px; width: 100px; height: 50px;
84
+ background-color: #0000ff; transform-origin: 0 0;
85
+ transform: translate(50%, 50%);
86
+ }
87
+ #d { transform-origin: top; }
88
+ #e { transform-origin: bottom right; }
89
+ #f { transform-origin: 10px; }
90
+ </style></head><body><div id="a"></div><div id="b"></div><div id="c"></div>
91
+ <div id="d"></div><div id="e"></div><div id="f"></div></body></html>
92
+ \`;
93
+
94
+ const win = createWindow({ title: "parse", width: 400, height: 300 });
95
+ win.on("ready", () => {
96
+ console.log("a=" + win.computedStyle("#a", "transform"));
97
+ console.log("b=" + win.computedStyle("#b", "transform"));
98
+ console.log("c=" + win.computedStyle("#c", "transform"));
99
+ /* translate(50%, 50%) resolves against the box: 50px, 25px. */
100
+ console.log("c-rect=" + win.getBoundingClientRect("#c").x.toFixed(1) + "," +
101
+ win.getBoundingClientRect("#c").y.toFixed(1));
102
+ /* A vertical keyword keeps x centred; bottom-right is right-bottom. */
103
+ console.log("d-origin=" + win.computedStyle("#d", "transform-origin"));
104
+ console.log("e-origin=" + win.computedStyle("#e", "transform-origin"));
105
+ console.log("f-origin=" + win.computedStyle("#f", "transform-origin"));
106
+ console.log(win.paintList().trim());
107
+ win.close();
108
+ });
109
+ win.loadHTML(HTML);
110
+ run();
111
+ `);
112
+ // `translate3d`/`scale3d` keep their x/y and drop `z`; `rotateZ` is `rotate`.
113
+ expect(value("a")).toBe("translate(10px, 4px) scale(2, 3) rotate(180deg)");
114
+ // An unsupported function invalidates the whole declaration.
115
+ expect(value("b")).toBe("none");
116
+ // Percentages stay unresolved in the computed value and resolve in layout.
117
+ expect(value("c")).toBe("translate(50%, 50%)");
118
+ expect(value("c-rect")).toBe("150.0,125.0");
119
+ expect(value("d-origin")).toBe("50% 0%");
120
+ expect(value("e-origin")).toBe("100% 100%");
121
+ expect(value("f-origin")).toBe("10px 50%");
122
+ expect(stdout).toContain("transform=matrix(1,0,0,1,50,25)");
123
+ });
124
+ it("hit tests through the transform", () => {
125
+ const { value } = harness.compileAndRun("transform-hit", `
126
+ import { createWindow, run } from "gui";
127
+
128
+ const HTML = \`
129
+ <html><head><style>
130
+ body { margin: 0; }
131
+ #box {
132
+ position: absolute; left: 100px; top: 100px; width: 100px; height: 50px;
133
+ background-color: #ff0000;
134
+ transform-origin: 0 0;
135
+ transform: rotate(90deg);
136
+ }
137
+ </style></head><body><div id="box"></div></body></html>
138
+ \`;
139
+
140
+ const win = createWindow({ title: "hit", width: 400, height: 300 });
141
+ win.on("ready", () => {
142
+ /* The rotated box covers x 50..150, y 100..200 and no longer covers the
143
+ * area it occupied before the rotation (x 100..200). */
144
+ console.log("rotated=" + win.hitTest(60, 190));
145
+ console.log("vacated=" + win.hitTest(190, 110));
146
+ win.close();
147
+ });
148
+ win.loadHTML(HTML);
149
+ run();
150
+ `);
151
+ expect(value("rotated")).toBe("div#box");
152
+ expect(value("vacated")).toBe("");
153
+ });
154
+ it("animates `transform` through a CSS transition", () => {
155
+ const { value } = harness.compileAndRun("transform-transition", `
156
+ import { createWindow, run } from "gui";
157
+
158
+ const HTML = \`
159
+ <html><head><style>
160
+ body { margin: 0; }
161
+ #box {
162
+ position: absolute; left: 20px; top: 200px; width: 60px; height: 30px;
163
+ background-color: #0000ff;
164
+ transform: translateX(0px) rotate(0deg);
165
+ transition: transform 400ms linear;
166
+ }
167
+ #box:hover { transform: translateX(40px) rotate(90deg); }
168
+ </style></head><body><div id="box"></div></body></html>
169
+ \`;
170
+
171
+ const win = createWindow({ title: "transition", width: 400, height: 300 });
172
+ win.on("ready", () => {
173
+ console.log("start=" + win.computedStyle("#box", "transform"));
174
+ win.sendEvent("mousemove", { x: 50, y: 215 });
175
+ win.advance(100);
176
+ console.log("q1=" + win.computedStyle("#box", "transform"));
177
+ win.advance(200);
178
+ console.log("q3=" + win.computedStyle("#box", "transform"));
179
+ win.advance(200);
180
+ console.log("end=" + win.computedStyle("#box", "transform"));
181
+ const end = win.getBoundingClientRect("#box");
182
+ console.log("end-rect=" + end.x.toFixed(1) + "," + end.y.toFixed(1) + "," +
183
+ end.width.toFixed(1) + "," + end.height.toFixed(1));
184
+ win.close();
185
+ });
186
+ win.loadHTML(HTML);
187
+ run();
188
+ `);
189
+ expect(value("start")).toBe("translateX(0px) rotate(0deg)");
190
+ // 100/400 and 300/400 of the way between the two lists, linearly.
191
+ expect(value("q1")).toBe("translateX(10px) rotate(22.5deg)");
192
+ expect(value("q3")).toBe("translateX(30px) rotate(67.5deg)");
193
+ expect(value("end")).toBe("translateX(40px) rotate(90deg)");
194
+ // Moved 40px right and rotated a quarter turn about its centre.
195
+ expect(value("end-rect")).toBe("75.0,185.0,30.0,60.0");
196
+ });
197
+ });
198
+ //# sourceMappingURL=gui-transform.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gui-transform.test.js","sourceRoot":"","sources":["../../../tests/e2e/gui-transform.test.ts"],"names":[],"mappings":"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"}
package/doc/ai/cli.md CHANGED
@@ -46,6 +46,11 @@ Parsing details that occasionally surprise:
46
46
  `run`, or is a positional for the other commands.
47
47
  - `-o` consumes the next argument unconditionally; the other value flags leave
48
48
  the value unset when the next argument starts with `-`.
49
+ - `-o`/`output` may name a directory that does not exist yet: the driver creates
50
+ the parent directories of the output path. (`--out` likewise creates its
51
+ directory.) Without that, an explicit path such as `-o dist/app.exe` where
52
+ `dist/` was missing used to fail inside the linker, since clang — not xbintsc —
53
+ is what writes the executable.
49
54
 
50
55
  ## Project config
51
56
 
@@ -67,10 +67,27 @@ import { createWindow, run } from "gui";
67
67
 
68
68
  It is a self-contained GPU-accelerated renderer (own HTML parser, CSS cascade,
69
69
  layout and compositor) — not a system WebView. It ships as a per-platform
70
- prebuilt `gui.a`/`gui.lib` because it is C++. If that archive is missing for
70
+ prebuilt `gui.a`/`gui.lib` because it is C++. `<img>`, `<video>` and `<audio>`
71
+ really decode: a minimal static FFmpeg is bundled into the archive, and the DOM
72
+ media API (`play`/`pause`/`currentTime`/`volume`/…) plus the
73
+ `loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error` events let a
74
+ program drive playback. There are no built-in controls — draw your own chrome
75
+ with elements and CSS (see `examples/gui/media.ts`). Video frames and audio PCM
76
+ are decoded on the main thread. `<canvas>` is supported too: `getContext("2d")`
77
+ returns a context whose fills, paths, text and `drawImage` are rasterised by a
78
+ self-written CPU rasteriser, and element handles expose `canvas` drawing plus
79
+ `width`/`height`. CSS `transform`/`transform-origin` work as paint-time
80
+ effects, so a box (and the text, images and children in it) can be translated,
81
+ scaled and rotated without reflowing anything — animate it per frame from
82
+ `requestAnimationFrame` or with `transition: transform` (see
83
+ `examples/gui/pelican-svg`). If that archive is missing for
71
84
  your platform, the build fails with an actionable message; design and roadmap
72
- live in [../gui.md](../gui.md), and the scripting surface in
73
- [../gui-scripts.md](../gui-scripts.md).
85
+ live in [../gui.md](../gui.md) (overview; the engine is documented by function
86
+ under [`doc/gui/`](../gui/)), and the scripting surface in
87
+ [../gui-scripts.md](../gui-scripts.md). Media playback is the one part that
88
+ Windows does not get: the Windows `gui.a` is built without FFmpeg and media
89
+ slots are inert. See [../gui/canvas.md](../gui/canvas.md) for the canvas API and
90
+ its limits.
74
91
 
75
92
  ## Third-party npm packages
76
93
 
@@ -64,9 +64,20 @@ import { createWindow, run } from "gui";
64
64
  ```
65
65
 
66
66
  它是自带渲染引擎的 GPU 加速实现(自己的 HTML 解析器、CSS 级联、布局与合成器),不是
67
- 系统 WebView。因为主体是 C++,它以各平台预编译的 `gui.a`/`gui.lib` 形式分发。如果该
68
- 平台的归档不存在,构建会以明确的提示失败;设计与路线图见 [../../zh-CN/gui.md](../../zh-CN/gui.md),
69
- 脚本接口见 [../../zh-CN/gui-scripts.md](../../zh-CN/gui-scripts.md)。
67
+ 系统 WebView。`<img>`、`<video>`、`<audio>` 都是真正解码:归档内打入一个最小化的
68
+ 静态 FFmpeg,DOM 媒体 API(`play`/`pause`/`currentTime`/`volume`/…)与
69
+ `loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error` 事件可让程序驱动播放。
70
+ 没有内置控件——用元素和 CSS 自绘控件(见 `examples/gui/media.ts`);视频帧与音频 PCM
71
+ 都在主线程解码。也支持 `<canvas>`:`getContext("2d")` 返回的上下文,其填充、路径、
72
+ 文本与 `drawImage` 由自研 CPU 光栅器处理,元素句柄还暴露 `width`/`height`。CSS
73
+ `transform`/`transform-origin` 作为绘制期效果可用,因此一个盒子(以及其中的文本、
74
+ 图像与子元素)可以被平移、缩放与旋转而不触发任何重排——可以用
75
+ `requestAnimationFrame` 逐帧驱动,或写成 `transition: transform`(见
76
+ `examples/gui/pelican-svg`)。因为主体是 C++,它以各平台预编译的 `gui.a`/`gui.lib` 形式分发。如果该
77
+ 平台的归档不存在,构建会以明确的提示失败;设计与路线图见 [../../zh-CN/gui.md](../../zh-CN/gui.md)(总览;引擎按功能拆分在 [`doc/zh-CN/gui/`](../../zh-CN/gui/) 下),
78
+ 脚本接口见 [../../zh-CN/gui-scripts.md](../../zh-CN/gui-scripts.md)。媒体播放是
79
+ Windows 唯一不支持的:Windows 的 `gui.a` 不构建 FFmpeg,媒体槽位为空操作。Canvas API
80
+ 及其限制见 [../../zh-CN/gui/canvas.md](../../zh-CN/gui/canvas.md)。
70
81
 
71
82
  ## 第三方 npm 包
72
83
 
@@ -0,0 +1,97 @@
1
+ # Animation: frames and transitions
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there.
5
+
6
+ ## Animation frames (M10)
7
+
8
+ `requestAnimationFrame` runs a callback once on the next frame; the callback
9
+ receives the frame timestamp (ms) and may mutate the DOM, which is restyled and
10
+ repainted in the same frame. Re-queue from inside the callback to animate:
11
+
12
+ ```ts
13
+ const win = createWindow({ title: "anim", width: 320, height: 240 });
14
+ let n = 0;
15
+ const tick = (t: number) => {
16
+ win.document.getElementById("label").textContent = String(n++);
17
+ if (n < 120) win.requestAnimationFrame(tick); // id returned; cancelAnimationFrame(id) drops it
18
+ };
19
+ win.on("ready", () => win.requestAnimationFrame(tick));
20
+ win.loadHTML("<div id='label'>0</div>");
21
+ run();
22
+ ```
23
+
24
+ ## CSS transitions (M6b)
25
+
26
+ `XtDocument` runs CSS transitions between the *target* computed styles (the
27
+ cascade for the current `:hover`/`:focus` state) and the *displayed* styles used
28
+ for layout and paint. When a state change alters a transitioned property, a
29
+ running transition is recorded and re-applied every frame until it finishes;
30
+ the engine advances the clock with real frame deltas (`XtDocument::advance`),
31
+ and `win.advance(ms)` lets tests step it deterministically.
32
+
33
+ - Supported properties: `background-color`, `color`, `border-color`,
34
+ `border-radius` and `transform`; `transition: all` covers them. A
35
+ layout-affecting property (width, position, …) is still not animated — that
36
+ would reflow every frame — but `transform` is paint-time, so animating it costs
37
+ one matrix per animated box per frame ([Layout](./layout.md#transforms-m15)).
38
+ - Both the `transition` shorthand and the `transition-property` / `-duration` /
39
+ `-delay` / `-timing-function` longhands are parsed; time values accept `s` and
40
+ `ms`; timing functions are `linear`, `ease`, `ease-in`, `ease-out` and
41
+ `ease-in-out` (`ease` is a smoothstep approximation).
42
+ - Retargeting mid-flight starts a new transition from the current interpolated
43
+ value, so reversing a hover animates smoothly from wherever it was.
44
+ - `computedStyle()` reports the displayed (interpolated) value, so transitions
45
+ are directly observable in tests.
46
+
47
+ `transform` lists interpolate **element-wise** (`document.cpp`,
48
+ `xt_transform_interpolate`), which is what makes the common pair
49
+ `translateY(-4px) rotate(-0.35deg)` <-> `none` glide instead of jumping: the
50
+ missing side is filled with identity ops of the same kind. Two lists whose shapes
51
+ or units do not line up (`rotate(10deg)` vs `rotate(10rad)`, or a different number
52
+ of functions) are not interpolated at all — the transition snaps to the target,
53
+ which is CSS's discrete fallback rather than a wrong-looking sweep.
54
+
55
+ ## CSS transforms (M15)
56
+
57
+ `transform` and `transform-origin` are supported as *paint-time* effects
58
+ (`translate`/`translate3d`/`translateX`/`translateY`,
59
+ `scale`/`scale3d`/`scaleX`/`scaleY`, `rotate`/`rotateZ`, `matrix`; the `3d`
60
+ spellings' `z` argument is parsed and dropped, since the engine is 2D;
61
+ `deg`/`rad`/`grad`/`turn`; `%`, `px`, `em`, `rem`, `vw`, `vh`). Layout
62
+ is not affected: geometry stays where flow put it, and only the painted result
63
+ moves, so a size, a scroll position or a sibling's position never depends on an
64
+ animation frame. See [Layout](./layout.md#transforms-m15) for the composition
65
+ rules and [Rendering](./rendering.md) for how it reaches the GPU.
66
+
67
+ Two ways to drive one:
68
+
69
+ ```ts
70
+ // 1. Per frame — a pose from the frame clock. Nothing reflows: the string is
71
+ // parsed into matrices and the display list picks them up in the same frame.
72
+ const tick = (t: number): void => {
73
+ const s = t / 1000;
74
+ art.style.setProperty(
75
+ "transform",
76
+ "translateY(" + (Math.sin(s * 2.4) * 5).toFixed(2) + "px) " +
77
+ "rotate(" + (Math.sin(s * 1.15) * 1.1).toFixed(2) + "deg)",
78
+ );
79
+ win.requestAnimationFrame(tick);
80
+ };
81
+ ```
82
+
83
+ ```css
84
+ /* 2. From a state change — a transitioned transform, like any other property. */
85
+ #caption {
86
+ transform-origin: 50% 100%;
87
+ transition: transform 260ms ease-out;
88
+ }
89
+ #caption:hover { transform: translateY(-2px) scale(1.03); }
90
+ ```
91
+
92
+ Both are exercised by `examples/gui/pelican-svg` (per-frame sway plus a
93
+ hover transition on the caption) and pinned by
94
+ `tests/e2e/gui-transform.test.ts`.
95
+
96
+ Still to do for animation: `@keyframes` animations and `cubic-bezier(...)`.
97
+