xbintsc 0.3.16 → 0.3.25

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 (57) hide show
  1. package/dist/src/driver/bundler/graph.d.ts +2 -1
  2. package/dist/src/driver/bundler/graph.js +18 -3
  3. package/dist/src/driver/bundler/graph.js.map +1 -1
  4. package/dist/src/driver/bundler/merge.d.ts +2 -1
  5. package/dist/src/driver/bundler/merge.js +2 -2
  6. package/dist/src/driver/bundler/merge.js.map +1 -1
  7. package/dist/src/driver/compiler.js +2 -2
  8. package/dist/src/driver/compiler.js.map +1 -1
  9. package/dist/src/extensions/gui/html.d.ts +25 -0
  10. package/dist/src/extensions/gui/html.js +203 -0
  11. package/dist/src/extensions/gui/html.js.map +1 -0
  12. package/dist/src/extensions/gui/index.js +11 -0
  13. package/dist/src/extensions/gui/index.js.map +1 -1
  14. package/dist/src/extensions/registry.d.ts +29 -0
  15. package/dist/src/extensions/registry.js +14 -0
  16. package/dist/src/extensions/registry.js.map +1 -1
  17. package/dist/src/index.d.ts +1 -1
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/lexer/scanner/identifiers.js +4 -12
  20. package/dist/src/lexer/scanner/identifiers.js.map +1 -1
  21. package/dist/tests/driver/bundler-resolve.test.d.ts +1 -0
  22. package/dist/tests/driver/bundler-resolve.test.js +230 -0
  23. package/dist/tests/driver/bundler-resolve.test.js.map +1 -0
  24. package/dist/tests/driver/modules.test.js +68 -0
  25. package/dist/tests/driver/modules.test.js.map +1 -1
  26. package/dist/tests/e2e/gui.test.js +210 -1
  27. package/dist/tests/e2e/gui.test.js.map +1 -1
  28. package/dist/tests/extensions/gui-index.test.d.ts +1 -0
  29. package/dist/tests/extensions/gui-index.test.js +49 -0
  30. package/dist/tests/extensions/gui-index.test.js.map +1 -0
  31. package/dist/tests/extensions/gui.test.d.ts +1 -0
  32. package/dist/tests/extensions/gui.test.js +177 -0
  33. package/dist/tests/extensions/gui.test.js.map +1 -0
  34. package/dist/tests/extensions/registry.test.js +36 -0
  35. package/dist/tests/extensions/registry.test.js.map +1 -1
  36. package/dist/tests/public-api.test.d.ts +1 -0
  37. package/dist/tests/public-api.test.js +31 -0
  38. package/dist/tests/public-api.test.js.map +1 -0
  39. package/package.json +1 -1
  40. package/runtime/ext_gui/document.cpp +90 -0
  41. package/runtime/ext_gui/document.h +23 -0
  42. package/runtime/ext_gui/dom.cpp +36 -0
  43. package/runtime/ext_gui/dom.h +7 -0
  44. package/runtime/ext_gui/dom_api.cpp +1247 -0
  45. package/runtime/ext_gui/dom_api.h +12 -0
  46. package/runtime/ext_gui/gui.cpp +49 -3
  47. package/runtime/ext_gui/gui_engine.h +54 -0
  48. package/runtime/ext_gui/script.cpp +63 -0
  49. package/runtime/ext_gui/window.cpp +60 -0
  50. package/src/driver/bundler/graph.ts +21 -2
  51. package/src/driver/bundler/merge.ts +3 -1
  52. package/src/driver/compiler.ts +2 -2
  53. package/src/extensions/gui/html.ts +236 -0
  54. package/src/extensions/gui/index.ts +11 -0
  55. package/src/extensions/registry.ts +39 -0
  56. package/src/index.ts +1 -1
  57. package/src/lexer/scanner/identifiers.ts +4 -11
@@ -0,0 +1,12 @@
1
+ /*
2
+ * xbintsc GUI engine — DOM object model entry points.
3
+ *
4
+ * The implementation lives in `dom_api.cpp`; this header only pulls in the
5
+ * shared engine definitions so translation units can include either one.
6
+ */
7
+ #ifndef XT_GUI_DOM_API_H
8
+ #define XT_GUI_DOM_API_H
9
+
10
+ #include "gui_engine.h"
11
+
12
+ #endif /* XT_GUI_DOM_API_H */
@@ -89,6 +89,9 @@ void xt_gui_quit_window(XtGuiWindow *win) {
89
89
  if (win == NULL || !win->open) return;
90
90
  win->open = 0;
91
91
  xt_gui_emit(win, "close");
92
+ win->animation_frames.clear();
93
+ xt_gui_handles_reset(win);
94
+ win->document_object = XT_UNDEFINED;
92
95
  win->document.reset();
93
96
  win->html.clear();
94
97
  if (g_device != NULL) {
@@ -111,6 +114,19 @@ void xt_gui_window_viewport(XtGuiWindow *win, float *width, float *height) {
111
114
  if (height != NULL) *height = (float)h;
112
115
  }
113
116
 
117
+ void xt_gui_flush_dom(XtGuiWindow *win) {
118
+ if (win == NULL || win->document == nullptr) return;
119
+ bool mutated = win->struct_dirty != 0;
120
+ win->struct_dirty = 0;
121
+ if (win->document->takeDirty()) mutated = true;
122
+ if (!mutated) return;
123
+ float width = 0;
124
+ float height = 0;
125
+ xt_gui_window_viewport(win, &width, &height);
126
+ win->document->restyle(width, height);
127
+ win->geometry.dirty = 1;
128
+ }
129
+
114
130
  static XtGuiWindow *xt_gui_find_by_window_id(SDL_WindowID id) {
115
131
  SDL_Window *window = SDL_GetWindowFromID(id);
116
132
  if (window == NULL) return NULL;
@@ -164,10 +180,13 @@ void xt_gui_dispatch_pointer(XtGuiWindow *win, const char *type, float x, float
164
180
  int clicks) {
165
181
  if (win == NULL || !win->open) return;
166
182
  xt_gui_update_hover(win, x, y);
183
+ const xtgui::Node *node = xt_gui_hit_node(win, x, y);
167
184
  if (strcmp(type, "mousedown") == 0 && win->document != nullptr) {
168
- if (win->document->setFocus(xt_gui_hit_node(win, x, y))) win->geometry.dirty = 1;
185
+ if (win->document->setFocus(node)) win->geometry.dirty = 1;
169
186
  }
170
- xt_gui_emit_payload(win, type, xt_gui_pointer_payload(type, win, x, y, button, clicks));
187
+ xt_value payload = xt_gui_pointer_payload(type, win, x, y, button, clicks);
188
+ if (node != nullptr) xt_gui_emit_dom_event(win, type, node, payload);
189
+ xt_gui_emit_payload(win, type, payload);
171
190
  }
172
191
 
173
192
  void xt_gui_dispatch_wheel(XtGuiWindow *win, float x, float y, float delta_x, float delta_y) {
@@ -175,6 +194,8 @@ void xt_gui_dispatch_wheel(XtGuiWindow *win, float x, float y, float delta_x, fl
175
194
  xt_value payload = xt_gui_pointer_payload("wheel", win, x, y, -1, 0);
176
195
  xt_object_set(payload, xt_string_from_cstr("deltaX"), xt_number(delta_x));
177
196
  xt_object_set(payload, xt_string_from_cstr("deltaY"), xt_number(delta_y));
197
+ const xtgui::Node *node = xt_gui_hit_node(win, x, y);
198
+ if (node != nullptr) xt_gui_emit_dom_event(win, "wheel", node, payload);
178
199
  xt_gui_emit_payload(win, "wheel", payload);
179
200
  }
180
201
 
@@ -194,7 +215,10 @@ static xt_value xt_gui_key_payload(const char *type, const char *key, const char
194
215
 
195
216
  void xt_gui_dispatch_key(XtGuiWindow *win, const char *type, const char *key, const char *code) {
196
217
  if (win == NULL || !win->open) return;
197
- xt_gui_emit_payload(win, type, xt_gui_key_payload(type, key, code, false, false, false, false, false));
218
+ xt_value payload = xt_gui_key_payload(type, key, code, false, false, false, false, false);
219
+ const xtgui::Node *node = win->document != nullptr ? win->document->focus() : nullptr;
220
+ if (node != nullptr) xt_gui_emit_dom_event(win, type, node, payload);
221
+ xt_gui_emit_payload(win, type, payload);
198
222
  }
199
223
 
200
224
  static void xt_gui_handle_event(const SDL_Event *event) {
@@ -265,6 +289,19 @@ static void xt_gui_handle_event(const SDL_Event *event) {
265
289
 
266
290
  /* -- rendering ------------------------------------------------------------ */
267
291
 
292
+ void xt_gui_run_animation_frames(XtGuiWindow *win, double timestamp_ms) {
293
+ if (win == NULL || win->animation_frames.empty()) return;
294
+ /* Swap the queue out first: a callback that calls requestAnimationFrame
295
+ * again schedules for the *next* frame, not this one. */
296
+ std::vector<XtGuiAnimationFrame> frames;
297
+ frames.swap(win->animation_frames);
298
+ for (const XtGuiAnimationFrame &frame : frames) {
299
+ if (!win->open) break;
300
+ xt_value arg = xt_number(timestamp_ms);
301
+ xt_call_with_this(frame.fn, win->object, 1, &arg);
302
+ }
303
+ }
304
+
268
305
  void xt_gui_render_window(XtGuiWindow *win) {
269
306
  if (win == NULL || !win->open || win->window == NULL || g_device == NULL) return;
270
307
  float viewport_width = 0;
@@ -275,8 +312,15 @@ void xt_gui_render_window(XtGuiWindow *win) {
275
312
  double now = (double)SDL_GetTicks();
276
313
  double delta = win->last_frame_ms > 0.0 ? now - win->last_frame_ms : 0.0;
277
314
  win->last_frame_ms = now;
315
+ /* Animation-frame callbacks run before layout, so any DOM mutation they make
316
+ * is picked up by this same frame. */
317
+ xt_gui_run_animation_frames(win, now);
278
318
  if (win->document != nullptr && win->document->advance(delta)) win->geometry.dirty = 1;
279
319
 
320
+ /* A DOM mutation from a handler invalidates the tree: restyle/relayout
321
+ * before (re)building the geometry. */
322
+ xt_gui_flush_dom(win);
323
+
280
324
  /* Rebuild the geometry only when the document or viewport changed. */
281
325
  bool can_paint = xt_gui_renderer_ensure(g_device, win->window);
282
326
  if (can_paint && win->geometry.dirty && win->document != nullptr) {
@@ -378,6 +422,8 @@ extern "C" xt_value xt_gui_create_window(int32_t argc, xt_value *argv) {
378
422
  record->text_geometry = XtGuiGeometry();
379
423
  record->image_geometry = XtGuiGeometry();
380
424
  record->last_frame_ms = 0.0;
425
+ record->animation_frames.clear();
426
+ record->next_animation_frame_id = 1;
381
427
  record->background[0] = 0.08f;
382
428
  record->background[1] = 0.09f;
383
429
  record->background[2] = 0.11f;
@@ -22,6 +22,8 @@ extern "C" {
22
22
  xt_value xt_gui_create_window(int32_t argc, xt_value *argv);
23
23
  xt_value xt_gui_run(int32_t argc, xt_value *argv);
24
24
  xt_value xt_gui_quit(int32_t argc, xt_value *argv);
25
+ /** `__registerScript(id, fn)`: record an AOT-compiled `<script>` body. */
26
+ xt_value xt_register_script(int32_t argc, xt_value *argv);
25
27
 
26
28
  #ifdef __cplusplus
27
29
  } /* extern "C" */
@@ -30,10 +32,26 @@ xt_value xt_gui_quit(int32_t argc, xt_value *argv);
30
32
 
31
33
  #include <memory>
32
34
  #include <string>
35
+ #include <unordered_map>
36
+ #include <vector>
33
37
 
34
38
  #include "document.h"
35
39
  #include "renderer.h"
36
40
 
41
+ /** One element-level event listener registered through `addEventListener`. */
42
+ struct XtGuiNodeListener {
43
+ std::string type;
44
+ xt_value fn;
45
+ bool once;
46
+ bool capture;
47
+ };
48
+
49
+ /** One pending `requestAnimationFrame` callback. */
50
+ struct XtGuiAnimationFrame {
51
+ int id;
52
+ xt_value fn;
53
+ };
54
+
37
55
  /* One live window. `object` is the JavaScript-visible handle; the record is
38
56
  * addressed from it through the hidden `__xt_gui_index` property. */
39
57
  struct XtGuiWindow {
@@ -50,10 +68,43 @@ struct XtGuiWindow {
50
68
  float background[4];
51
69
  int width;
52
70
  int height;
71
+
72
+ /* -- DOM object model (element handles + events) ----------------------- */
73
+ /** `document` handle for this window (lazily created). */
74
+ xt_value document_object = XT_UNDEFINED;
75
+ /** Bumped on every `loadHTML`; stale handles no-op. */
76
+ double doc_generation = 1.0;
77
+ /** Node -> table index / cached handle, so identity is stable. */
78
+ std::vector<xtgui::Node *> node_order;
79
+ std::unordered_map<const xtgui::Node *, int> node_index;
80
+ std::unordered_map<const xtgui::Node *, xt_value> node_handles;
81
+ /** Element-level listeners, keyed by node. */
82
+ std::unordered_map<const xtgui::Node *, std::vector<XtGuiNodeListener>> node_listeners;
83
+ /** Set by a DOM mutation; consumed by the frame loop before repainting. */
84
+ int struct_dirty = 0;
85
+ /** Callbacks queued for the next frame, plus the id counter. */
86
+ std::vector<XtGuiAnimationFrame> animation_frames;
87
+ int next_animation_frame_id = 1;
53
88
  };
54
89
 
55
90
  /** Resolve a window handle (`this`) to its record, or NULL for a foreign value. */
56
91
  XtGuiWindow *xt_gui_window_from_this(xt_value thisValue);
92
+ /** The `document` handle for `win` (created on first use). */
93
+ xt_value xt_gui_document_handle(XtGuiWindow *win);
94
+ /** Node handle for `node` (created and cached on first use). */
95
+ xt_value xt_gui_node_handle(XtGuiWindow *win, const xtgui::Node *node);
96
+ /** Drop every cached handle/listener and bump the document generation. */
97
+ void xt_gui_handles_reset(XtGuiWindow *win);
98
+ /** Deliver `type` to element listeners along the propagation path. */
99
+ void xt_gui_emit_dom_event(XtGuiWindow *win, const char *type, const xtgui::Node *target,
100
+ xt_value legacy_payload);
101
+ /** Synthesise a DOM event whose target is `target` (used by element.click()). */
102
+ void xt_gui_dispatch_to_node(XtGuiWindow *win, const xtgui::Node *target, const char *type);
103
+ /** Apply any pending DOM mutation (restyle/relayout) before a synchronous read. */
104
+ void xt_gui_flush_dom(XtGuiWindow *win);
105
+ /** Run every registered `<script data-xt-id>` body found in the document, in
106
+ * document order, passing `(windowHandle, documentHandle)`. */
107
+ void xt_gui_run_scripts(XtGuiWindow *win);
57
108
  /** Shared prototype carrying the window methods. */
58
109
  xt_value xt_gui_window_proto(void);
59
110
  /** Release GPU claim + destroy the window and mark the record closed. */
@@ -64,6 +115,9 @@ void xt_gui_emit(XtGuiWindow *win, const char *event);
64
115
  void xt_gui_emit_payload(XtGuiWindow *win, const char *event, xt_value payload);
65
116
  /** Acquire a swapchain frame for `win` and clear it (placeholder paint). */
66
117
  void xt_gui_render_window(XtGuiWindow *win);
118
+ /** Run every callback queued with `requestAnimationFrame` for the frame at
119
+ * `timestamp_ms`, then clear the queue. */
120
+ void xt_gui_run_animation_frames(XtGuiWindow *win, double timestamp_ms);
67
121
  /** Current logical window size (CSS viewport), in pixels. */
68
122
  void xt_gui_window_viewport(XtGuiWindow *win, float *width, float *height);
69
123
  /** CSS-like descriptor of the deepest element at (x, y), or `""` (test/debug). */
@@ -0,0 +1,63 @@
1
+ /*
2
+ * xbintsc GUI engine — AOT `<script>` registry.
3
+ *
4
+ * xbintsc has no runtime interpreter, so `<script>` bodies are compiled ahead
5
+ * of time. The `.html` asset loader (see `src/extensions/gui/html.ts`) extracts
6
+ * each inline body, wraps it in a function `(window, document) => { ... }` and
7
+ * calls the `__registerScript(id, fn)` builtin at module initialization. This
8
+ * file keeps that registry and runs the matching functions once the document
9
+ * has been parsed.
10
+ */
11
+
12
+ #include "gui_engine.h"
13
+
14
+ #include <string>
15
+ #include <unordered_map>
16
+ #include <vector>
17
+
18
+ /** Script id -> AOT-compiled function value. Lives for the whole process (the
19
+ * runtime arena never reclaims, so storing `xt_value`s is safe). */
20
+ static std::unordered_map<std::string, xt_value> &script_registry() {
21
+ static std::unordered_map<std::string, xt_value> registry;
22
+ return registry;
23
+ }
24
+
25
+ extern "C" xt_value xt_register_script(int32_t argc, xt_value *argv) {
26
+ if (argc >= 2) {
27
+ xt_value id_value = xt_to_string(xt_arg(argc, argv, 0));
28
+ const char *id = xt_string_data(id_value);
29
+ xt_value fn = xt_arg(argc, argv, 1);
30
+ if (id != nullptr && XT_IS_FUNCTION(fn)) {
31
+ script_registry()[std::string(id)] = fn;
32
+ }
33
+ }
34
+ return XT_UNDEFINED;
35
+ }
36
+
37
+ /** Collect `data-xt-id` markers from `<script>` elements, in document order. */
38
+ static void collect_script_ids(const xtgui::Node *node, std::vector<std::string> &ids) {
39
+ if (node == nullptr) return;
40
+ if (node->isElement() && node->isTag("script")) {
41
+ const std::string *id = node->attr("data-xt-id");
42
+ if (id != nullptr && !id->empty()) ids.push_back(*id);
43
+ return;
44
+ }
45
+ for (const std::unique_ptr<xtgui::Node> &child : node->children) {
46
+ collect_script_ids(child.get(), ids);
47
+ }
48
+ }
49
+
50
+ void xt_gui_run_scripts(XtGuiWindow *win) {
51
+ if (win == nullptr || win->document == nullptr) return;
52
+ std::vector<std::string> ids;
53
+ collect_script_ids(win->document->root(), ids);
54
+ if (ids.empty()) return;
55
+ xt_value window_handle = win->object;
56
+ xt_value document_handle = xt_gui_document_handle(win);
57
+ for (const std::string &id : ids) {
58
+ auto it = script_registry().find(id);
59
+ if (it == script_registry().end()) continue;
60
+ xt_value args[2] = {window_handle, document_handle};
61
+ xt_call_with_this(it->second, XT_UNDEFINED, 2, args);
62
+ }
63
+ }
@@ -10,6 +10,8 @@
10
10
 
11
11
  #include "gui_engine.h"
12
12
 
13
+ #include <algorithm>
14
+
13
15
  #include "paint.h"
14
16
  #include "text.h"
15
17
 
@@ -48,16 +50,23 @@ static xt_value win_load_html(xt_value self, xt_value env, int32_t argc, xt_valu
48
50
  XtGuiWindow *win = xt_gui_window_from_this(self);
49
51
  if (win != nullptr) {
50
52
  win->html = html;
53
+ /* Fresh document: invalidate every previously handed-out handle. */
54
+ xt_gui_handles_reset(win);
51
55
  win->document = std::make_unique<xtgui::XtDocument>();
52
56
  float width = 0;
53
57
  float height = 0;
54
58
  xt_gui_window_viewport(win, &width, &height);
55
59
  win->document->load(html, width, height);
56
60
  win->geometry.dirty = 1;
61
+ /* Run the AOT-compiled `<script>` bodies now that the tree is parsed, so
62
+ * `document.getElementById(...)` resolves, then settle the first layout. */
63
+ xt_gui_run_scripts(win);
64
+ xt_gui_flush_dom(win);
57
65
  }
58
66
  xt_object_set(self, xt_string_from_cstr("__xt_gui_html"), xt_string_from_cstr(html.c_str()));
59
67
  /* Painting the document is milestone M3b+; the parsed tree and computed
60
68
  * styles are available now (see `computedStyle`). */
69
+ xt_gui_emit(win, "DOMContentLoaded");
61
70
  xt_gui_emit(win, "load");
62
71
  return XT_UNDEFINED;
63
72
  }
@@ -69,12 +78,21 @@ static xt_value win_get_html(xt_value self, xt_value env, int32_t argc, xt_value
69
78
  return xt_object_get_cstr(self, "__xt_gui_html");
70
79
  }
71
80
 
81
+ /** The `document` handle for this window (created on first access). */
82
+ static xt_value win_document_get(xt_value self, xt_value env, int32_t argc, xt_value *argv) {
83
+ (void)env;
84
+ (void)argc;
85
+ (void)argv;
86
+ return xt_gui_document_handle(xt_gui_window_from_this(self));
87
+ }
88
+
72
89
  /** Diagnostic/test hook: computed value of `property` for the first match of
73
90
  * `selector`, or an empty string. */
74
91
  static xt_value win_computed_style(xt_value self, xt_value env, int32_t argc, xt_value *argv) {
75
92
  (void)env;
76
93
  XtGuiWindow *win = xt_gui_window_from_this(self);
77
94
  if (win == nullptr || win->document == nullptr || argc < 2) return xt_string_from_cstr("");
95
+ xt_gui_flush_dom(win);
78
96
  xt_value selectorValue = xt_to_string(xt_arg(argc, argv, 0));
79
97
  xt_value propertyValue = xt_to_string(xt_arg(argc, argv, 1));
80
98
  const char *selector = xt_string_data(selectorValue);
@@ -95,6 +113,7 @@ static xt_value win_document_tree(xt_value self, xt_value env, int32_t argc, xt_
95
113
  (void)argv;
96
114
  XtGuiWindow *win = xt_gui_window_from_this(self);
97
115
  if (win == nullptr || win->document == nullptr) return xt_string_from_cstr("");
116
+ xt_gui_flush_dom(win);
98
117
  std::string text = win->document->toDebugString();
99
118
  return xt_string_from_cstr(text.c_str());
100
119
  }
@@ -104,6 +123,7 @@ static xt_value win_query_count(xt_value self, xt_value env, int32_t argc, xt_va
104
123
  (void)env;
105
124
  XtGuiWindow *win = xt_gui_window_from_this(self);
106
125
  if (win == nullptr || win->document == nullptr || argc < 1) return xt_number(0);
126
+ xt_gui_flush_dom(win);
107
127
  xt_value selectorValue = xt_to_string(xt_arg(argc, argv, 0));
108
128
  const char *selector = xt_string_data(selectorValue);
109
129
  if (selector == nullptr) return xt_number(0);
@@ -116,6 +136,7 @@ static xt_value win_get_bounding_rect(xt_value self, xt_value env, int32_t argc,
116
136
  (void)env;
117
137
  XtGuiWindow *win = xt_gui_window_from_this(self);
118
138
  if (win == nullptr || win->document == nullptr || argc < 1) return XT_UNDEFINED;
139
+ xt_gui_flush_dom(win);
119
140
  xt_value selectorValue = xt_to_string(xt_arg(argc, argv, 0));
120
141
  const char *selector = xt_string_data(selectorValue);
121
142
  if (selector == nullptr) return XT_UNDEFINED;
@@ -138,6 +159,7 @@ static xt_value win_layout_tree(xt_value self, xt_value env, int32_t argc, xt_va
138
159
  (void)argv;
139
160
  XtGuiWindow *win = xt_gui_window_from_this(self);
140
161
  if (win == nullptr || win->document == nullptr) return xt_string_from_cstr("");
162
+ xt_gui_flush_dom(win);
141
163
  std::string text = win->document->layout().dump();
142
164
  return xt_string_from_cstr(text.c_str());
143
165
  }
@@ -169,6 +191,7 @@ static xt_value win_paint_list(xt_value self, xt_value env, int32_t argc, xt_val
169
191
  (void)argv;
170
192
  XtGuiWindow *win = xt_gui_window_from_this(self);
171
193
  if (win == nullptr || win->document == nullptr) return xt_string_from_cstr("");
194
+ xt_gui_flush_dom(win);
172
195
  xtgui::DisplayList list;
173
196
  xtgui::xt_paint_build(win->document->layout().root(), list);
174
197
  std::string text = list.dump();
@@ -182,6 +205,7 @@ static xt_value win_paint_count(xt_value self, xt_value env, int32_t argc, xt_va
182
205
  (void)argv;
183
206
  XtGuiWindow *win = xt_gui_window_from_this(self);
184
207
  if (win == nullptr || win->document == nullptr) return xt_number(0);
208
+ xt_gui_flush_dom(win);
185
209
  xtgui::DisplayList list;
186
210
  xtgui::xt_paint_build(win->document->layout().root(), list);
187
211
  return xt_number((double)list.rects.size());
@@ -259,6 +283,34 @@ static xt_value win_is_open(xt_value self, xt_value env, int32_t argc, xt_value
259
283
  return (win != NULL && win->open) ? XT_TRUE : XT_FALSE;
260
284
  }
261
285
 
286
+ /** `requestAnimationFrame(fn)` -> id. The callback runs once on the next frame
287
+ * with the frame timestamp (ms) as its only argument. */
288
+ static xt_value win_request_animation_frame(xt_value self, xt_value env, int32_t argc,
289
+ xt_value *argv) {
290
+ (void)env;
291
+ XtGuiWindow *win = xt_gui_window_from_this(self);
292
+ if (win == NULL || !win->open) return xt_number(0);
293
+ xt_value fn = xt_arg(argc, argv, 0);
294
+ if (!XT_IS_FUNCTION(fn)) return xt_number(0);
295
+ int id = win->next_animation_frame_id++;
296
+ win->animation_frames.push_back(XtGuiAnimationFrame{id, fn});
297
+ return xt_number((double)id);
298
+ }
299
+
300
+ /** `cancelAnimationFrame(id)` drops a callback queued for the next frame. */
301
+ static xt_value win_cancel_animation_frame(xt_value self, xt_value env, int32_t argc,
302
+ xt_value *argv) {
303
+ (void)env;
304
+ XtGuiWindow *win = xt_gui_window_from_this(self);
305
+ if (win == NULL || argc < 1) return XT_UNDEFINED;
306
+ int id = (int)xt_to_number(xt_arg(argc, argv, 0));
307
+ std::vector<XtGuiAnimationFrame> &frames = win->animation_frames;
308
+ frames.erase(std::remove_if(frames.begin(), frames.end(),
309
+ [id](const XtGuiAnimationFrame &frame) { return frame.id == id; }),
310
+ frames.end());
311
+ return XT_UNDEFINED;
312
+ }
313
+
262
314
  static xt_value win_on(xt_value self, xt_value env, int32_t argc, xt_value *argv) {
263
315
  (void)env;
264
316
  if (argc >= 2) {
@@ -314,6 +366,7 @@ static xt_value win_hit_test(xt_value self, xt_value env, int32_t argc, xt_value
314
366
  (void)env;
315
367
  XtGuiWindow *win = xt_gui_window_from_this(self);
316
368
  if (win == nullptr || win->document == nullptr || argc < 2) return xt_string_from_cstr("");
369
+ xt_gui_flush_dom(win);
317
370
  float x = (float)xt_to_number(xt_arg(argc, argv, 0));
318
371
  float y = (float)xt_to_number(xt_arg(argc, argv, 1));
319
372
  std::string target = xt_gui_hit_test(win, x, y);
@@ -376,10 +429,15 @@ static void define_method(xt_value proto, const char *name, void *fn) {
376
429
  xt_object_set(proto, xt_string_from_cstr(name), xt_closure_new(fn, 0, NULL));
377
430
  }
378
431
 
432
+ static void define_getter(xt_value proto, const char *name, void *fn) {
433
+ xt_object_define_getter(proto, xt_string_from_cstr(name), xt_closure_new(fn, 0, NULL));
434
+ }
435
+
379
436
  xt_value xt_gui_window_proto(void) {
380
437
  if (XT_IS_OBJECT(g_window_proto)) return g_window_proto;
381
438
  xt_value proto = xt_object_new();
382
439
  define_method(proto, "setTitle", (void *)win_set_title);
440
+ define_getter(proto, "document", (void *)win_document_get);
383
441
  define_method(proto, "setSize", (void *)win_set_size);
384
442
  define_method(proto, "loadHTML", (void *)win_load_html);
385
443
  define_method(proto, "getHTML", (void *)win_get_html);
@@ -396,6 +454,8 @@ xt_value xt_gui_window_proto(void) {
396
454
  define_method(proto, "fontMetrics", (void *)win_font_metrics);
397
455
  define_method(proto, "close", (void *)win_close);
398
456
  define_method(proto, "isOpen", (void *)win_is_open);
457
+ define_method(proto, "requestAnimationFrame", (void *)win_request_animation_frame);
458
+ define_method(proto, "cancelAnimationFrame", (void *)win_cancel_animation_frame);
399
459
  define_method(proto, "on", (void *)win_on);
400
460
  define_method(proto, "off", (void *)win_off);
401
461
  define_method(proto, "hitTest", (void *)win_hit_test);
@@ -4,12 +4,13 @@
4
4
  */
5
5
 
6
6
  import { existsSync, readFileSync } from "node:fs";
7
- import { dirname } from "node:path";
7
+ import { dirname, extname } from "node:path";
8
8
  import { SyntaxKind, type ExportDeclaration, type ImportDeclaration, type Statement } from "../../ast/nodes.js";
9
9
  import { bind } from "../../binder/binder.js";
10
10
  import { DiagnosticBag, DiagnosticCode } from "../../diagnostics/diagnostic.js";
11
11
  import { SourceFile } from "../../diagnostics/source.js";
12
12
  import { Parser } from "../../parser/parser.js";
13
+ import type { AssetLoader } from "../../extensions/registry.js";
13
14
  import { classifyDependency } from "./resolve.js";
14
15
  import type { ModuleRecord } from "./types.js";
15
16
 
@@ -29,6 +30,7 @@ export function loadGraph(
29
30
  entryPath: string,
30
31
  diagnostics: DiagnosticBag,
31
32
  externalSpecifiers: ReadonlySet<string>,
33
+ assetLoaders: Readonly<Record<string, AssetLoader>> = {},
32
34
  ): ModuleRecord[] | undefined {
33
35
  const records = new Map<string, ModuleRecord>();
34
36
  const order: ModuleRecord[] = [];
@@ -50,7 +52,24 @@ export function loadGraph(
50
52
  return undefined;
51
53
  }
52
54
  visiting.add(path);
53
- const text = readFileSync(path, "utf8");
55
+ const raw = readFileSync(path, "utf8");
56
+ /* An extension may rewrite an imported asset (e.g. `.html`) into a TS
57
+ * module before it is parsed. The core only sees the transformed text. */
58
+ const loader = assetLoaders[extname(path).toLowerCase()];
59
+ let text = raw;
60
+ if (loader) {
61
+ try {
62
+ text = loader(path, raw).moduleSource;
63
+ } catch (error) {
64
+ diagnostics.error(
65
+ DiagnosticCode.CodegenError,
66
+ error instanceof Error ? error.message : String(error),
67
+ );
68
+ failed = true;
69
+ visiting.delete(path);
70
+ return undefined;
71
+ }
72
+ }
54
73
  const file = new SourceFile(path, text);
55
74
  const parser = new Parser(file, diagnostics);
56
75
  const sourceFile = parser.parseSourceFile();
@@ -27,6 +27,7 @@ import {
27
27
  import type { BindingName } from "../../ast/declarations.js";
28
28
  import { SymbolKind } from "../../binder/binder.js";
29
29
  import type { DiagnosticBag } from "../../diagnostics/diagnostic.js";
30
+ import type { AssetLoader } from "../../extensions/registry.js";
30
31
  import { loadGraph } from "./graph.js";
31
32
  import { classifyDependency } from "./resolve.js";
32
33
  import {
@@ -43,8 +44,9 @@ export function bundleModules(
43
44
  entryPath: string,
44
45
  diagnostics: DiagnosticBag,
45
46
  externalSpecifiers: ReadonlySet<string> = new Set(),
47
+ assetLoaders: Readonly<Record<string, AssetLoader>> = {},
46
48
  ): BundleResult | undefined {
47
- const records = loadGraph(entryPath, diagnostics, externalSpecifiers);
49
+ const records = loadGraph(entryPath, diagnostics, externalSpecifiers, assetLoaders);
48
50
  if (!records) return undefined;
49
51
 
50
52
  const byPath = new Map(records.map((record) => [record.path, record]));
@@ -109,7 +109,7 @@ export function compileEntry(entryPath: string, extensions?: ExtensionRegistry):
109
109
  statement.kind === SyntaxKind.ExportAssignment,
110
110
  );
111
111
  if (isModule) {
112
- const bundled = bundleModules(absoluteEntry, diagnostics, externalModuleSpecifiers(registry));
112
+ const bundled = bundleModules(absoluteEntry, diagnostics, externalModuleSpecifiers(registry), registry.assetLoaders());
113
113
  if (bundled) sourceFile = bundled.sourceFile;
114
114
  }
115
115
  const { ir } = generate(sourceFile, diagnostics, {
@@ -185,7 +185,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
185
185
  );
186
186
  let cacheText = sourceText;
187
187
  if (isModule) {
188
- const bundled = bundleModules(absoluteEntry, diagnostics, externalModuleSpecifiers(registry));
188
+ const bundled = bundleModules(absoluteEntry, diagnostics, externalModuleSpecifiers(registry), registry.assetLoaders());
189
189
  if (bundled) {
190
190
  sourceFile = bundled.sourceFile;
191
191
  cacheText = bundled.text;