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,193 @@
1
+ /*
2
+ * xbintsc GUI engine — `<video>` / `<audio>` lifecycle.
3
+ *
4
+ * This unit bridges three pieces:
5
+ * - the DOM (`Node`) and the per-window media table on `XtGuiWindow`;
6
+ * - the decoders (`media.cpp`, backed by FFmpeg + SDL audio);
7
+ * - the renderer (`renderer_image.cpp`), which owns the GPU textures.
8
+ *
9
+ * The frame loop calls `xt_gui_media_sync` (open/close decoders so the next
10
+ * layout sees the current sources and intrinsic sizes) and `xt_gui_media_tick`
11
+ * (decode for the elapsed wall clock and fire the resulting DOM events) once
12
+ * per frame. No `xt_value` is stored here, so there is nothing for the
13
+ * collector to trace.
14
+ */
15
+
16
+ #include "gui_internal.h"
17
+
18
+ #include <string>
19
+ #include <unordered_set>
20
+ #include <vector>
21
+
22
+ namespace {
23
+
24
+ bool isMediaElement(const xtgui::Node *node) {
25
+ return node != nullptr && node->isElement() && (node->isTag("video") || node->isTag("audio"));
26
+ }
27
+
28
+ /** The element's `src`, or the `src` of its first child `<source>` (which is
29
+ * how a `<video>` with fallback formats is written). */
30
+ std::string sourceOf(const xtgui::Node *node) {
31
+ if (!isMediaElement(node)) return "";
32
+ const std::string *src = node->attr("src");
33
+ if (src != nullptr && !src->empty()) return *src;
34
+ for (const std::unique_ptr<xtgui::Node> &child : node->children) {
35
+ if (child->isElement() && child->isTag("source")) {
36
+ const std::string *child_src = child->attr("src");
37
+ if (child_src != nullptr && !child_src->empty()) return *child_src;
38
+ }
39
+ }
40
+ return "";
41
+ }
42
+
43
+ bool booleanAttr(const xtgui::Node *node, const char *name) {
44
+ return node->attr(name) != nullptr;
45
+ }
46
+
47
+ void collectMedia(const xtgui::Node *node, std::vector<const xtgui::Node *> *out) {
48
+ if (node == nullptr) return;
49
+ if (isMediaElement(node)) {
50
+ out->push_back(node);
51
+ return; // a media element's children are `<source>`/`<track>`, not media
52
+ }
53
+ for (const std::unique_ptr<xtgui::Node> &child : node->children) collectMedia(child.get(), out);
54
+ }
55
+
56
+ void releasePlayerTexture(const xtgui::MediaPlayer *player) {
57
+ if (player != nullptr && guidev::g_device != nullptr) {
58
+ xt_gui_media_texture_release(guidev::g_device, player->id());
59
+ }
60
+ }
61
+
62
+ /** Open a decoder for `node`/`src`, apply its boolean attributes, record it and
63
+ * return it (or NULL, after firing `error`). */
64
+ xtgui::MediaPlayer *openEntry(XtGuiWindow *win, const xtgui::Node *node, const std::string &src) {
65
+ XtGuiMediaEntry entry;
66
+ entry.src = src;
67
+ std::string error;
68
+ entry.player = xtgui::MediaPlayer::open(src, &error);
69
+ if (!entry.player) {
70
+ entry.failed = true;
71
+ win->media_players[node] = std::move(entry);
72
+ xt_gui_dispatch_to_node(win, node, "error");
73
+ return nullptr;
74
+ }
75
+ entry.player->set_loop(booleanAttr(node, "loop"));
76
+ entry.player->set_muted(booleanAttr(node, "muted"));
77
+ entry.player->set_autoplay(booleanAttr(node, "autoplay"));
78
+ xtgui::MediaPlayer *raw = entry.player.get();
79
+ win->media_players[node] = std::move(entry);
80
+ return raw;
81
+ }
82
+
83
+ void emitMediaEvents(XtGuiWindow *win, const xtgui::Node *node, unsigned events) {
84
+ if (events == 0) return;
85
+ if (events & xtgui::kMediaEventLoadedMetadata) {
86
+ xt_gui_dispatch_to_node(win, node, "loadedmetadata");
87
+ }
88
+ if (events & xtgui::kMediaEventPlay) xt_gui_dispatch_to_node(win, node, "play");
89
+ if (events & xtgui::kMediaEventPause) xt_gui_dispatch_to_node(win, node, "pause");
90
+ if (events & xtgui::kMediaEventEnded) xt_gui_dispatch_to_node(win, node, "ended");
91
+ if (events & xtgui::kMediaEventTimeUpdate) xt_gui_dispatch_to_node(win, node, "timeupdate");
92
+ if (events & xtgui::kMediaEventError) xt_gui_dispatch_to_node(win, node, "error");
93
+ }
94
+
95
+ } // namespace
96
+
97
+ xtgui::MediaPlayer *xt_gui_media_player(XtGuiWindow *win, const xtgui::Node *node) {
98
+ if (win == nullptr || !isMediaElement(node)) return nullptr;
99
+ auto found = win->media_players.find(node);
100
+ if (found != win->media_players.end()) return found->second.player.get();
101
+ std::string src = sourceOf(node);
102
+ if (src.empty()) return nullptr;
103
+ return openEntry(win, node, src);
104
+ }
105
+
106
+ void xt_gui_media_forget(XtGuiWindow *win, const xtgui::Node *node) {
107
+ if (win == nullptr) return;
108
+ auto found = win->media_players.find(node);
109
+ if (found == win->media_players.end()) return;
110
+ releasePlayerTexture(found->second.player.get());
111
+ win->media_players.erase(found);
112
+ }
113
+
114
+ void xt_gui_media_sync(XtGuiWindow *win) {
115
+ if (win == nullptr || win->document == nullptr) return;
116
+ std::vector<const xtgui::Node *> nodes;
117
+ collectMedia(win->document->root(), &nodes);
118
+ std::unordered_set<const xtgui::Node *> live(nodes.begin(), nodes.end());
119
+
120
+ /* Drop decoders whose element left the tree. Comparing pointers only (never
121
+ * dereferencing) is safe for a node that was already destroyed. */
122
+ for (auto it = win->media_players.begin(); it != win->media_players.end();) {
123
+ if (live.count(it->first) == 0) {
124
+ releasePlayerTexture(it->second.player.get());
125
+ it = win->media_players.erase(it);
126
+ } else {
127
+ ++it;
128
+ }
129
+ }
130
+
131
+ /* Open decoders for new sources and reopen when an element's `src` changes. */
132
+ for (const xtgui::Node *node : nodes) {
133
+ const std::string src = sourceOf(node);
134
+ auto found = win->media_players.find(node);
135
+ if (src.empty()) {
136
+ if (found != win->media_players.end()) {
137
+ releasePlayerTexture(found->second.player.get());
138
+ win->media_players.erase(found);
139
+ }
140
+ continue;
141
+ }
142
+ if (found != win->media_players.end() && found->second.src == src) continue;
143
+ if (found != win->media_players.end()) {
144
+ releasePlayerTexture(found->second.player.get());
145
+ win->media_players.erase(found);
146
+ }
147
+ openEntry(win, node, src);
148
+ }
149
+
150
+ /* Publish the intrinsic size of every `<video>` so layout can use it. */
151
+ for (const xtgui::Node *node : nodes) {
152
+ if (!node->isTag("video")) continue;
153
+ auto found = win->media_players.find(node);
154
+ xtgui::MediaPlayer *player = found == win->media_players.end() ? nullptr : found->second.player.get();
155
+ float width = 0.0f;
156
+ float height = 0.0f;
157
+ uint64_t id = 0;
158
+ if (player != nullptr) {
159
+ width = (float)player->frame_width();
160
+ height = (float)player->frame_height();
161
+ id = player->id();
162
+ }
163
+ win->document->setMediaSize(node, width, height, id);
164
+ }
165
+ }
166
+
167
+ void xt_gui_media_tick(XtGuiWindow *win, double now_ms) {
168
+ if (win == nullptr) return;
169
+ /* Snapshot the keys: a handler may change an element's `src`, which erases and
170
+ * reinserts into the table while we iterate. */
171
+ std::vector<const xtgui::Node *> nodes;
172
+ nodes.reserve(win->media_players.size());
173
+ for (const auto &entry : win->media_players) nodes.push_back(entry.first);
174
+ for (const xtgui::Node *node : nodes) {
175
+ auto found = win->media_players.find(node);
176
+ if (found == win->media_players.end() || !found->second.player) continue;
177
+ unsigned events = found->second.player->tick(now_ms);
178
+ /* A freshly decoded frame must reach the image upload. The geometry pass
179
+ * rebuilds the image batches, so mark it dirty once per new generation. */
180
+ if (found->second.player->has_video() &&
181
+ found->second.player->frame_generation() != found->second.rendered_generation) {
182
+ found->second.rendered_generation = found->second.player->frame_generation();
183
+ win->geometry.dirty = 1;
184
+ }
185
+ emitMediaEvents(win, node, events);
186
+ }
187
+ }
188
+
189
+ void xt_gui_media_shutdown(XtGuiWindow *win) {
190
+ if (win == nullptr) return;
191
+ for (auto &entry : win->media_players) releasePlayerTexture(entry.second.player.get());
192
+ win->media_players.clear();
193
+ }
@@ -13,8 +13,11 @@
13
13
  #include "stb_image.h"
14
14
 
15
15
  #include <cctype>
16
+ #include <string>
16
17
  #include <unordered_map>
17
18
 
19
+ #include "svg.h"
20
+
18
21
  namespace xtgui {
19
22
  namespace {
20
23
 
@@ -63,31 +66,44 @@ std::string normalizePath(const std::string &input) {
63
66
 
64
67
  } // namespace
65
68
 
66
- const Image *xt_image_load(const std::string &raw_path) {
69
+ const Image *xt_image_load(const std::string &raw_path, int target_width, int target_height) {
67
70
  std::string path = normalizePath(raw_path);
71
+ bool svg = xt_svg_is_svg(path);
72
+ /* Raster formats always decode at their intrinsic resolution, so their cache
73
+ * key is the path. SVG is rasterised at the drawn pixel size, which is part
74
+ * of its identity. */
75
+ std::string key = path;
76
+ if (svg && (target_width > 0 || target_height > 0)) {
77
+ key += "#" + std::to_string(target_width) + "x" + std::to_string(target_height);
78
+ }
68
79
  auto &cache = imageCache();
69
- auto found = cache.find(path);
80
+ auto found = cache.find(key);
70
81
  if (found != cache.end()) {
71
82
  return found->second.width > 0 ? &found->second : nullptr;
72
83
  }
73
84
 
74
85
  Image image;
75
- int width = 0;
76
- int height = 0;
77
- int channels = 0;
78
- unsigned char *data = stbi_load(path.c_str(), &width, &height, &channels, 4);
79
- if (data != nullptr) {
80
- image.width = width;
81
- image.height = height;
82
- image.pixels.assign(data, data + (size_t)width * (size_t)height * 4);
83
- stbi_image_free(data);
86
+ if (svg) {
87
+ xt_svg_render(path, target_width, target_height, &image);
88
+ } else {
89
+ int width = 0;
90
+ int height = 0;
91
+ int channels = 0;
92
+ unsigned char *data = stbi_load(path.c_str(), &width, &height, &channels, 4);
93
+ if (data != nullptr) {
94
+ image.width = width;
95
+ image.height = height;
96
+ image.pixels.assign(data, data + (size_t)width * (size_t)height * 4);
97
+ stbi_image_free(data);
98
+ }
84
99
  }
85
- auto inserted = cache.emplace(path, std::move(image));
100
+ auto inserted = cache.emplace(key, std::move(image));
86
101
  return inserted.first->second.width > 0 ? &inserted.first->second : nullptr;
87
102
  }
88
103
 
89
104
  bool xt_image_size(const std::string &raw_path, int *width, int *height) {
90
105
  std::string path = normalizePath(raw_path);
106
+ if (xt_svg_is_svg(path)) return xt_svg_size(path, width, height);
91
107
  auto &cache = sizeCache();
92
108
  auto found = cache.find(path);
93
109
  if (found == cache.end()) {
@@ -117,9 +133,12 @@ bool xt_image_decode(const unsigned char *data, size_t size, Image *out) {
117
133
  return true;
118
134
  }
119
135
 
136
+ bool xt_image_is_vector(const std::string &path) { return xt_svg_is_svg(normalizePath(path)); }
137
+
120
138
  void xt_image_shutdown() {
121
139
  imageCache().clear();
122
140
  sizeCache().clear();
141
+ xt_svg_shutdown();
123
142
  }
124
143
 
125
144
  } // namespace xtgui
@@ -23,18 +23,31 @@ struct Image {
23
23
  std::vector<unsigned char> pixels;
24
24
  };
25
25
 
26
- /** Decode an image file (PNG/JPEG/BMP/GIF/TGA) to RGBA8. Returns nullptr on
27
- * failure. Results (including failures) are cached by path. A `file://` prefix
28
- * and URL percent-encoding are handled. */
29
- const Image *xt_image_load(const std::string &path);
30
-
31
- /** Read just the intrinsic size from the file header, without decoding.
32
- * Returns false when the file cannot be read/parsed. Cached. */
26
+ /** Decode an image file (PNG/JPEG/BMP/GIF/TGA or SVG) to RGBA8. Returns
27
+ * nullptr on failure. Results (including failures) are cached by path. A
28
+ * `file://` prefix and URL percent-encoding are handled.
29
+ *
30
+ * `target_width`/`target_height` are the pixel size the image will be drawn at.
31
+ * Raster formats ignore them (they decode at their intrinsic resolution), but
32
+ * SVG is a vector format and is rasterised at (approximately) that size —
33
+ * preserving its aspect ratio — so a scaled `<img src="logo.svg">` stays crisp
34
+ * instead of being a magnified bitmap. Pass `<= 0` (the default) to use the intrinsic size. The cache is keyed by the
35
+ * resulting pixel size for SVG and by path for raster formats. */
36
+ const Image *xt_image_load(const std::string &path, int target_width = 0, int target_height = 0);
37
+
38
+ /** Read just the intrinsic size from the file header, without decoding. For an
39
+ * SVG this parses the document (the size comes from width/height or the
40
+ * viewBox). Returns false when the file cannot be read/parsed. Cached. */
33
41
  bool xt_image_size(const std::string &path, int *width, int *height);
34
42
 
35
43
  /** Decode an in-memory buffer to RGBA8 (e.g. a future `data:` URI). */
36
44
  bool xt_image_decode(const unsigned char *data, size_t size, Image *out);
37
45
 
46
+ /** True when `path` is a vector source (SVG) that is rasterised on demand
47
+ * rather than decoded as a bitmap. The renderer uses this to key vector
48
+ * textures by pixel size. */
49
+ bool xt_image_is_vector(const std::string &path);
50
+
38
51
  void xt_image_shutdown();
39
52
 
40
53
  } // namespace xtgui
@@ -19,13 +19,19 @@
19
19
  #include <cstring>
20
20
 
21
21
  #include "image.h"
22
+ #include "style_internal.h"
22
23
 
23
24
  namespace xtgui {
24
25
  namespace {
25
26
 
27
+ /** Compose `transform`/`transform-origin` down the tree (defined below). */
28
+ void applyTransforms(LayoutBox *box, const Matrix2D &parent_world, bool parent_transformed,
29
+ float viewportWidth, float viewportHeight);
30
+
26
31
  std::unique_ptr<LayoutBox> buildBoxTree(const Node *node,
27
32
  const std::unordered_map<const Node *, XtStyle> &styles,
28
- const XtStyle *inherited) {
33
+ const XtStyle *inherited, const MediaSizeMap *media,
34
+ const CanvasInfoMap *canvases) {
29
35
  if (node->isText()) {
30
36
  auto box = std::make_unique<LayoutBox>();
31
37
  box->node = node;
@@ -55,9 +61,53 @@ std::unique_ptr<LayoutBox> buildBoxTree(const Node *node,
55
61
  box->intrinsic_height = (float)height;
56
62
  }
57
63
  }
64
+ } else if (node->isElement() && node->isTag("video")) {
65
+ /* Default the video box to the HTML 300x150 until the decoder reports the
66
+ * real pixel size (fetched as soon as the element is associated with a
67
+ * player, before the next layout pass). */
68
+ box->is_image = true;
69
+ box->is_media = true;
70
+ box->intrinsic_width = 300;
71
+ box->intrinsic_height = 150;
72
+ if (media != nullptr) {
73
+ auto found = media->find(node);
74
+ if (found != media->end()) {
75
+ box->media_id = found->second.id;
76
+ if (found->second.width > 0 && found->second.height > 0) {
77
+ box->intrinsic_width = found->second.width;
78
+ box->intrinsic_height = found->second.height;
79
+ }
80
+ }
81
+ }
82
+ } else if (node->isElement() && node->isTag("canvas")) {
83
+ /* A replaced inline-block whose pixels are a script-owned bitmap. The
84
+ * intrinsic size is the HTML 300x150 default, overridden by the
85
+ * `width`/`height` attributes (or a script assignment to `canvas.width`),
86
+ * published by the window as `CanvasInfo`. */
87
+ box->is_canvas = true;
88
+ box->intrinsic_width = 300;
89
+ box->intrinsic_height = 150;
90
+ float parsed = 0.0f;
91
+ const std::string *attr_width = node->attr("width");
92
+ const std::string *attr_height = node->attr("height");
93
+ if (attr_width != nullptr && xt_css_parse_number(*attr_width, &parsed) && parsed > 0.0f)
94
+ box->intrinsic_width = parsed;
95
+ if (attr_height != nullptr && xt_css_parse_number(*attr_height, &parsed) && parsed > 0.0f)
96
+ box->intrinsic_height = parsed;
97
+ if (canvases != nullptr) {
98
+ auto found = canvases->find(node);
99
+ if (found != canvases->end()) {
100
+ box->canvas_id = found->second.id;
101
+ if (found->second.width > 0 && found->second.height > 0) {
102
+ box->intrinsic_width = found->second.width;
103
+ box->intrinsic_height = found->second.height;
104
+ }
105
+ }
106
+ }
58
107
  }
59
108
  for (const std::unique_ptr<Node> &child : node->children) {
60
- std::unique_ptr<LayoutBox> childBox = buildBoxTree(child.get(), styles, style);
109
+ std::unique_ptr<LayoutBox> childBox =
110
+ buildBoxTree(child.get(), styles, style, media, canvases);
61
111
  if (childBox != nullptr) {
62
112
  childBox->parent = box.get();
63
113
  box->children.push_back(std::move(childBox));
@@ -86,7 +136,8 @@ std::string dumpBox(const LayoutBox *box, int depth) {
86
136
  } // namespace
87
137
 
88
138
  void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *, XtStyle> &styles,
89
- float viewportWidth, float viewportHeight) {
139
+ float viewportWidth, float viewportHeight, const MediaSizeMap *media,
140
+ const CanvasInfoMap *canvases) {
90
141
  root_.reset();
91
142
  index_.clear();
92
143
  root_style_ = xt_style_initial();
@@ -105,14 +156,15 @@ void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *
105
156
  if (root != nullptr) {
106
157
  if (root->type == NodeType::Document) {
107
158
  for (const std::unique_ptr<Node> &child : root->children) {
108
- std::unique_ptr<LayoutBox> box = buildBoxTree(child.get(), styles, &root_style_);
159
+ std::unique_ptr<LayoutBox> box =
160
+ buildBoxTree(child.get(), styles, &root_style_, media, canvases);
109
161
  if (box != nullptr) {
110
162
  box->parent = root_.get();
111
163
  root_->children.push_back(std::move(box));
112
164
  }
113
165
  }
114
166
  } else {
115
- std::unique_ptr<LayoutBox> box = buildBoxTree(root, styles, &root_style_);
167
+ std::unique_ptr<LayoutBox> box = buildBoxTree(root, styles, &root_style_, media, canvases);
116
168
  if (box != nullptr) {
117
169
  box->parent = root_.get();
118
170
  root_->children.push_back(std::move(box));
@@ -120,6 +172,7 @@ void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *
120
172
  }
121
173
  }
122
174
  xt_layout_run(root_.get(), styles, viewportWidth, viewportHeight);
175
+ applyTransforms(root_.get(), Matrix2D(), false, viewportWidth, viewportHeight);
123
176
 
124
177
  std::vector<const LayoutBox *> stack;
125
178
  stack.push_back(root_.get());
@@ -144,7 +197,11 @@ const LayoutBox *hitTestBox(const LayoutBox *box, float x, float y) {
144
197
  for (auto it = box->children.rbegin(); it != box->children.rend(); ++it) {
145
198
  if (const LayoutBox *hit = hitTestBox(it->get(), x, y)) return hit;
146
199
  }
147
- if (x >= box->x && x < box->x + box->width && y >= box->y && y < box->y + box->height) {
200
+ float local_x = x;
201
+ float local_y = y;
202
+ if (box->has_transform) box->inverse.apply(x, y, &local_x, &local_y);
203
+ if (local_x >= box->x && local_x < box->x + box->width && local_y >= box->y &&
204
+ local_y < box->y + box->height) {
148
205
  return box;
149
206
  }
150
207
  return nullptr;
@@ -160,4 +217,71 @@ std::string LayoutTree::dump() const {
160
217
  return root_ == nullptr ? std::string() : dumpBox(root_.get(), 0);
161
218
  }
162
219
 
220
+ /* -- transforms ----------------------------------------------------------- */
221
+
222
+ namespace {
223
+
224
+ /** The transform a single box applies to its own geometry, relative to its
225
+ * `transform-origin`. Text boxes inherit their parent's style pointer, so only
226
+ * element boxes may contribute one (otherwise the style would be applied
227
+ * twice). */
228
+ Matrix2D boxLocalTransform(const LayoutBox *box, float viewportWidth, float viewportHeight) {
229
+ Matrix2D local;
230
+ if (box->style == nullptr || box->style->transform.empty()) return local;
231
+ if (box->node == nullptr || !box->node->isElement()) return local;
232
+ const XtStyle &style = *box->style;
233
+ Matrix2D body = xt_transform_matrix(style.transform, box->width, box->height, style.font_size,
234
+ viewportWidth, viewportHeight);
235
+ float origin_x = box->x + xt_style_resolve_absolute(style.transform_origin_x, box->width,
236
+ style.font_size, 16.0f, viewportWidth,
237
+ viewportHeight);
238
+ float origin_y = box->y + xt_style_resolve_absolute(style.transform_origin_y, box->height,
239
+ style.font_size, 16.0f, viewportWidth,
240
+ viewportHeight);
241
+ return Matrix2D::translate(origin_x, origin_y)
242
+ .multiply(body)
243
+ .multiply(Matrix2D::translate(-origin_x, -origin_y));
244
+ }
245
+
246
+ void applyTransforms(LayoutBox *box, const Matrix2D &parent_world, bool parent_transformed,
247
+ float viewportWidth, float viewportHeight) {
248
+ Matrix2D local = boxLocalTransform(box, viewportWidth, viewportHeight);
249
+ box->world = parent_world.multiply(local);
250
+ box->has_transform = parent_transformed || !local.identity();
251
+ if (!box->world.invert(&box->inverse)) box->inverse = Matrix2D();
252
+ for (std::unique_ptr<LayoutBox> &child : box->children) {
253
+ applyTransforms(child.get(), box->world, box->has_transform, viewportWidth, viewportHeight);
254
+ }
255
+ }
256
+
257
+ } // namespace
258
+
259
+ LayoutRect xt_layout_transform_rect(const Matrix2D &matrix, float x, float y, float width,
260
+ float height) {
261
+ LayoutRect out{x, y, width, height};
262
+ if (matrix.identity()) return out;
263
+ const float corners_x[4] = {x, x + width, x + width, x};
264
+ const float corners_y[4] = {y, y, y + height, y + height};
265
+ float min_x = 0, min_y = 0, max_x = 0, max_y = 0;
266
+ for (int i = 0; i < 4; i++) {
267
+ float corner_x = 0;
268
+ float corner_y = 0;
269
+ matrix.apply(corners_x[i], corners_y[i], &corner_x, &corner_y);
270
+ if (i == 0 || corner_x < min_x) min_x = corner_x;
271
+ if (i == 0 || corner_x > max_x) max_x = corner_x;
272
+ if (i == 0 || corner_y < min_y) min_y = corner_y;
273
+ if (i == 0 || corner_y > max_y) max_y = corner_y;
274
+ }
275
+ out.x = min_x;
276
+ out.y = min_y;
277
+ out.width = max_x - min_x;
278
+ out.height = max_y - min_y;
279
+ return out;
280
+ }
281
+
282
+ LayoutRect xt_layout_transformed_bounds(const LayoutBox *box) {
283
+ if (box == nullptr) return LayoutRect();
284
+ return xt_layout_transform_rect(box->world, box->x, box->y, box->width, box->height);
285
+ }
286
+
163
287
  } // namespace xtgui
@@ -13,6 +13,7 @@
13
13
  #ifndef XT_GUI_LAYOUT_H
14
14
  #define XT_GUI_LAYOUT_H
15
15
 
16
+ #include <cstdint>
16
17
  #include <memory>
17
18
  #include <string>
18
19
  #include <unordered_map>
@@ -23,6 +24,25 @@
23
24
 
24
25
  namespace xtgui {
25
26
 
27
+ /** Intrinsic size and decoder id of one `<video>` element, resolved by the
28
+ * engine and keyed by node. `width`/`height` are the decoded pixel size, or
29
+ * the 300x150 default before metadata is available. */
30
+ struct MediaSize {
31
+ float width = 0;
32
+ float height = 0;
33
+ uint64_t id = 0;
34
+ };
35
+ using MediaSizeMap = std::unordered_map<const Node *, MediaSize>;
36
+
37
+ /** Intrinsic size and render id of one `<canvas>` element, resolved by the
38
+ * engine and keyed by node. `width`/`height` are the bitmap's pixel size. */
39
+ struct CanvasInfo {
40
+ float width = 0;
41
+ float height = 0;
42
+ uint64_t id = 0;
43
+ };
44
+ using CanvasInfoMap = std::unordered_map<const Node *, CanvasInfo>;
45
+
26
46
  /** A positioned text fragment (a wrapped text box can span several lines).
27
47
  * `text` holds the run this fragment covers (a word or a run of spaces). */
28
48
  struct TextFragment {
@@ -33,6 +53,11 @@ struct TextFragment {
33
53
  std::string text;
34
54
  };
35
55
 
56
+ /** A viewport-relative rectangle (`getBoundingClientRect` result). */
57
+ struct LayoutRect {
58
+ float x = 0, y = 0, width = 0, height = 0;
59
+ };
60
+
36
61
  struct LayoutBox {
37
62
  const Node *node = nullptr;
38
63
  const XtStyle *style = nullptr;
@@ -47,6 +72,17 @@ struct LayoutBox {
47
72
  float intrinsic_width = 0;
48
73
  float intrinsic_height = 0;
49
74
 
75
+ /* `<video>`: a replaced element whose pixels come from a live decoder
76
+ * (`media_id`) instead of a file. `is_image` is set too so the
77
+ * replaced-element sizing in `layout_flow.cpp` applies unchanged. */
78
+ bool is_media = false;
79
+ uint64_t media_id = 0;
80
+
81
+ /* `<canvas>`: a replaced element whose pixels are a script-owned `Canvas2D`
82
+ * (`canvas_id`). `is_image`-style intrinsic sizing applies. */
83
+ bool is_canvas = false;
84
+ uint64_t canvas_id = 0;
85
+
50
86
  Display display = Display::Inline;
51
87
 
52
88
  /* Resolved (px) box edges. */
@@ -63,6 +99,13 @@ struct LayoutBox {
63
99
  * is none): the containing block `position: absolute` resolves against. */
64
100
  float cb_x = 0, cb_y = 0, cb_width = 0, cb_height = 0;
65
101
 
102
+ /* `transform`/`transform-origin` composed down the tree: `world` maps this
103
+ * box's layout coordinates to device space, `inverse` does the opposite (hit
104
+ * testing). `has_transform` is true for a box inside a transformed subtree. */
105
+ Matrix2D world;
106
+ Matrix2D inverse;
107
+ bool has_transform = false;
108
+
66
109
  LayoutBox *parent = nullptr;
67
110
  std::vector<std::unique_ptr<LayoutBox>> children;
68
111
  };
@@ -70,7 +113,8 @@ struct LayoutBox {
70
113
  class LayoutTree {
71
114
  public:
72
115
  void compute(const Node *root, const std::unordered_map<const Node *, XtStyle> &styles,
73
- float viewportWidth, float viewportHeight);
116
+ float viewportWidth, float viewportHeight, const MediaSizeMap *media = nullptr,
117
+ const CanvasInfoMap *canvases = nullptr);
74
118
  void clear() {
75
119
  root_.reset();
76
120
  index_.clear();
@@ -92,6 +136,15 @@ class LayoutTree {
92
136
  /** Text width under `style` (shaped with the HarfBuzz/FreeType stack). */
93
137
  float xt_layout_text_width(const std::string &text, const XtStyle &style);
94
138
 
139
+ /** The box's border box in device space: its layout rectangle pushed through
140
+ * `box->world` and reduced to an axis-aligned bounding box. */
141
+ LayoutRect xt_layout_transformed_bounds(const LayoutBox *box);
142
+
143
+ /** Push an arbitrary layout rectangle through `matrix` and return its
144
+ * axis-aligned bounding box. */
145
+ LayoutRect xt_layout_transform_rect(const Matrix2D &matrix, float x, float y, float width,
146
+ float height);
147
+
95
148
  } // namespace xtgui
96
149
 
97
150
  #endif /* XT_GUI_LAYOUT_H */