xbintsc 0.5.36 → 0.5.37

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 (66) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/driver/version.d.ts +1 -1
  3. package/dist/src/driver/version.js +1 -1
  4. package/dist/src/extensions/gui/index.js +12 -0
  5. package/dist/src/extensions/gui/index.js.map +1 -1
  6. package/dist/tests/e2e/gui-browser.test.js +65 -0
  7. package/dist/tests/e2e/gui-browser.test.js.map +1 -1
  8. package/dist/tests/e2e/gui-canvas-args.test.d.ts +6 -0
  9. package/dist/tests/e2e/gui-canvas-args.test.js +217 -0
  10. package/dist/tests/e2e/gui-canvas-args.test.js.map +1 -0
  11. package/dist/tests/e2e/gui-canvas.test.js.map +1 -1
  12. package/dist/tests/e2e/gui-dom.test.js +215 -0
  13. package/dist/tests/e2e/gui-dom.test.js.map +1 -1
  14. package/dist/tests/e2e/gui-events.test.js +112 -0
  15. package/dist/tests/e2e/gui-events.test.js.map +1 -1
  16. package/doc/gui/api.md +38 -9
  17. package/doc/gui/canvas.md +49 -18
  18. package/doc/gui/dom.md +51 -4
  19. package/doc/zh-CN/gui/api.md +33 -4
  20. package/doc/zh-CN/gui/canvas.md +44 -19
  21. package/doc/zh-CN/gui/dom.md +45 -3
  22. package/package.json +7 -7
  23. package/runtime/ext_gui/browser.cpp +34 -49
  24. package/runtime/ext_gui/browser_abort.cpp +249 -0
  25. package/runtime/ext_gui/browser_dialogs.cpp +76 -0
  26. package/runtime/ext_gui/browser_media.cpp +176 -0
  27. package/runtime/ext_gui/browser_navigation.cpp +130 -0
  28. package/runtime/ext_gui/canvas.cpp +88 -30
  29. package/runtime/ext_gui/canvas.h +74 -9
  30. package/runtime/ext_gui/canvas_image_data.cpp +143 -0
  31. package/runtime/ext_gui/canvas_internal.h +8 -5
  32. package/runtime/ext_gui/canvas_pattern.cpp +90 -0
  33. package/runtime/ext_gui/canvas_raster.cpp +31 -9
  34. package/runtime/ext_gui/canvas_style.cpp +4 -2
  35. package/runtime/ext_gui/dom.h +4 -0
  36. package/runtime/ext_gui/dom_api.cpp +1 -0
  37. package/runtime/ext_gui/dom_api_canvas.cpp +120 -114
  38. package/runtime/ext_gui/dom_api_canvas_extra.cpp +57 -2
  39. package/runtime/ext_gui/dom_api_canvas_image.cpp +239 -0
  40. package/runtime/ext_gui/dom_api_canvas_paths.cpp +179 -0
  41. package/runtime/ext_gui/dom_api_computed_style.cpp +124 -0
  42. package/runtime/ext_gui/dom_api_dataset.cpp +154 -0
  43. package/runtime/ext_gui/dom_api_document.cpp +58 -5
  44. package/runtime/ext_gui/dom_api_events.cpp +93 -17
  45. package/runtime/ext_gui/dom_api_internal.h +55 -2
  46. package/runtime/ext_gui/dom_api_mutation.cpp +159 -0
  47. package/runtime/ext_gui/dom_api_node.cpp +40 -19
  48. package/runtime/ext_gui/dom_api_node_read.cpp +37 -1
  49. package/runtime/ext_gui/dom_api_path2d.cpp +358 -0
  50. package/runtime/ext_gui/dom_api_proto.cpp +35 -0
  51. package/runtime/ext_gui/dom_api_style.cpp +223 -13
  52. package/runtime/ext_gui/gui.cpp +59 -5
  53. package/runtime/ext_gui/gui_engine.h +47 -0
  54. package/runtime/ext_gui/gui_events.cpp +80 -9
  55. package/runtime/ext_gui/listener_options.h +43 -0
  56. package/runtime/ext_gui/paint.cpp +28 -0
  57. package/runtime/ext_gui/paint.h +4 -0
  58. package/runtime/ext_gui/window.cpp +14 -65
  59. package/runtime/ext_gui/window_events.cpp +125 -0
  60. package/runtime/rt.h +26 -0
  61. package/runtime/rt_internal.h +4 -0
  62. package/runtime/xt_builtins.c +0 -21
  63. package/runtime/xt_containers/access.inc +94 -2
  64. package/runtime/xt_containers/objects.inc +29 -31
  65. package/src/driver/version.ts +1 -1
  66. package/src/extensions/gui/index.ts +12 -0
@@ -93,17 +93,31 @@ METHOD(doc_create_element) {
93
93
  return xt_gui_node_handle(win, win->document->createElement(tag));
94
94
  }
95
95
 
96
- /** `createElementNS(ns, name)`: the engine has no namespaces, so the qualified
97
- * name after `:` is what gets created (`createElementNS(svgNS, "svg:rect")`). */
96
+ /** `createElementNS(ns, name)`: the namespace is recorded (so `namespaceURI`
97
+ * answers it) and the qualified name's prefix is preserved. HTML names are
98
+ * lowercased, but names in another namespace keep their case — SVG is
99
+ * case-sensitive (`linearGradient`, `clipPath`). */
98
100
  METHOD(doc_create_element_ns) {
99
101
  (void)env;
100
102
  XtGuiWindow *win = resolve_window(self);
101
103
  if (win == nullptr || win->document == nullptr || argc < 2) return XT_UNDEFINED;
104
+ std::string ns = to_string_value(xt_arg(argc, argv, 0));
102
105
  std::string name = to_string_value(xt_arg(argc, argv, 1));
106
+ std::string prefix;
103
107
  size_t colon = name.rfind(':');
104
- if (colon != std::string::npos) name = name.substr(colon + 1);
105
- for (char &c : name) c = (char)std::tolower((unsigned char)c);
106
- return xt_gui_node_handle(win, win->document->createElement(name));
108
+ if (colon != std::string::npos) {
109
+ prefix = name.substr(0, colon);
110
+ name = name.substr(colon + 1);
111
+ }
112
+ const bool html = ns.empty() || ns == "http://www.w3.org/1999/xhtml";
113
+ if (html) {
114
+ for (char &c : name) c = (char)std::tolower((unsigned char)c);
115
+ }
116
+ xtgui::Node *node = win->document->createElement(name);
117
+ if (node == nullptr) return XT_UNDEFINED;
118
+ node->ns = ns;
119
+ node->ns_prefix = prefix;
120
+ return xt_gui_node_handle(win, node);
107
121
  }
108
122
 
109
123
  /** `document.hidden` / `visibilityState`: a window in the main loop is always
@@ -124,6 +138,15 @@ METHOD(doc_visibility_state_get) {
124
138
  return xt_string_from_cstr("visible");
125
139
  }
126
140
 
141
+ /** `document.hasFocus()`: true after `window.focus()` until `window.blur()`. */
142
+ METHOD(doc_has_focus) {
143
+ (void)env;
144
+ (void)argc;
145
+ (void)argv;
146
+ XtGuiWindow *win = resolve_window(self);
147
+ return win != nullptr && win->focused ? XT_TRUE : XT_FALSE;
148
+ }
149
+
127
150
  /** `document.readyState`: the page is fully parsed before any script runs. */
128
151
  METHOD(doc_ready_state_get) {
129
152
  (void)self;
@@ -149,6 +172,36 @@ METHOD(doc_title_get) {
149
172
  return xt_string_from_cstr(text.c_str());
150
173
  }
151
174
 
175
+ /** `document.title = text`: replace the text of the document's `<title>`.
176
+ * A page without one gets a `<title>` (and a `<head>`, if the parser did not
177
+ * create one), so the assignment is not silently lost. */
178
+ METHOD(doc_title_set) {
179
+ (void)env;
180
+ XtGuiWindow *win = resolve_window(self);
181
+ if (win == nullptr || win->document == nullptr) return XT_UNDEFINED;
182
+ std::string text;
183
+ if (argc >= 1) text = to_string_value(xt_arg(argc, argv, 0));
184
+ xtgui::Node *root = const_cast<xtgui::Node *>(win->document->root());
185
+ xtgui::Node *title = const_cast<xtgui::Node *>(find_by_tag(root, "title"));
186
+ if (title == nullptr) {
187
+ xtgui::Node *head = const_cast<xtgui::Node *>(find_by_tag(root, "head"));
188
+ if (head == nullptr) {
189
+ head = win->document->createElement("head");
190
+ if (head == nullptr) return XT_UNDEFINED;
191
+ /* `<head>` belongs before the body, not after it. */
192
+ xtgui::Node *body = const_cast<xtgui::Node *>(find_by_tag(root, "body"));
193
+ win->document->insertBefore(root, head, body);
194
+ }
195
+ title = win->document->createElement("title");
196
+ if (title == nullptr) return XT_UNDEFINED;
197
+ win->document->appendChild(head, title);
198
+ }
199
+ title->children.clear();
200
+ if (!text.empty()) title->addText(text);
201
+ mark_dirty(win);
202
+ return XT_UNDEFINED;
203
+ }
204
+
152
205
  METHOD(doc_default_view_get) {
153
206
  (void)env;
154
207
  (void)argc;
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  #include "dom_api_internal.h"
7
+ #include "listener_options.h"
7
8
 
8
9
  #include <string>
9
10
  #include <vector>
@@ -15,8 +16,14 @@ static xt_value make_dom_event(XtGuiWindow *win, const char *type, const xtgui::
15
16
  xt_value event = xt_object_new_with_proto(event_proto());
16
17
  xt_object_set(event, xt_string_from_cstr("type"), xt_string_from_cstr(type));
17
18
  xt_object_set(event, xt_string_from_cstr("target"), xt_gui_node_handle(win, target));
19
+ xt_object_set(event, xt_string_from_cstr("currentTarget"), XT_NULL);
18
20
  xt_object_set(event, xt_string_from_cstr("defaultPrevented"), XT_FALSE);
19
21
  xt_object_set(event, xt_string_from_cstr("bubbles"), XT_TRUE);
22
+ xt_object_set(event, xt_string_from_cstr("cancelable"), XT_TRUE);
23
+ xt_object_set(event, xt_string_from_cstr("composed"), XT_TRUE);
24
+ xt_object_set(event, xt_string_from_cstr("isTrusted"), XT_TRUE);
25
+ xt_object_set(event, xt_string_from_cstr("eventPhase"), xt_number(0));
26
+ xt_object_set(event, xt_string_from_cstr("timeStamp"), xt_number((double)SDL_GetTicks()));
20
27
  if (XT_IS_OBJECT(legacy)) {
21
28
  static const char *const fields[] = {
22
29
  "x", "y", "clientX", "clientY", "button", "clicks",
@@ -31,23 +38,43 @@ static xt_value make_dom_event(XtGuiWindow *win, const char *type, const xtgui::
31
38
  return event;
32
39
  }
33
40
 
34
- /** A detached event object, as `new Event(type)` / `new CustomEvent(type, init)`
35
- * builds: no target yet, but the standard fields and `detail`. */
41
+ /** Set `field` to `value` unless `init` already supplied one. */
42
+ static void default_field(xt_value event, const char *field, xt_value value) {
43
+ if (XT_IS_UNDEFINED(xt_object_get_cstr(event, field))) {
44
+ xt_object_set(event, xt_string_from_cstr(field), value);
45
+ }
46
+ }
47
+
48
+ /** A detached event object, as `new Event(type, init)` /
49
+ * `new CustomEvent(type, init)` (and the other `*Event` constructors) builds:
50
+ * no target yet, the standard fields, and every member of `init` — so
51
+ * `composed`, `detail` and the mouse/keyboard fields all survive construction
52
+ * instead of being silently dropped. */
36
53
  xt_value event_construct(xt_value type, xt_value init) {
37
54
  std::string name = to_string_value(type);
38
55
  xt_value event = xt_object_new_with_proto(event_proto());
56
+ if (XT_IS_OBJECT(init)) {
57
+ xt_value keys = xt_object_keys(init);
58
+ int32_t count = (int32_t)xt_to_number(xt_array_length(keys));
59
+ for (int32_t i = 0; i < count; i++) {
60
+ xt_value key = xt_array_get(keys, xt_number((double)i));
61
+ xt_object_set(event, key, xt_object_get(init, key));
62
+ }
63
+ }
39
64
  xt_object_set(event, xt_string_from_cstr("type"), xt_string_from_cstr(name.c_str()));
40
- xt_object_set(event, xt_string_from_cstr("target"), XT_NULL);
41
- xt_object_set(event, xt_string_from_cstr("currentTarget"), XT_NULL);
42
- xt_object_set(event, xt_string_from_cstr("defaultPrevented"), XT_FALSE);
43
- xt_value bubbles = xt_object_get_cstr(init, "bubbles");
44
- xt_object_set(event, xt_string_from_cstr("bubbles"), XT_IS_UNDEFINED(bubbles) ? XT_FALSE : bubbles);
45
- xt_value cancelable = xt_object_get_cstr(init, "cancelable");
46
- xt_object_set(event, xt_string_from_cstr("cancelable"),
47
- XT_IS_UNDEFINED(cancelable) ? XT_FALSE : cancelable);
48
- xt_value detail = xt_object_get_cstr(init, "detail");
49
- xt_object_set(event, xt_string_from_cstr("detail"),
50
- XT_IS_UNDEFINED(detail) ? XT_NULL : detail);
65
+ default_field(event, "target", XT_NULL);
66
+ default_field(event, "currentTarget", XT_NULL);
67
+ default_field(event, "defaultPrevented", XT_FALSE);
68
+ default_field(event, "bubbles", XT_FALSE);
69
+ default_field(event, "cancelable", XT_FALSE);
70
+ default_field(event, "composed", XT_FALSE);
71
+ default_field(event, "isTrusted", XT_FALSE);
72
+ default_field(event, "eventPhase", xt_number(0));
73
+ default_field(event, "detail", XT_NULL);
74
+ default_field(event, "timeStamp", xt_number((double)SDL_GetTicks()));
75
+ /* Marks an event object built here, so `dispatchEvent(ev)` can reuse it (an
76
+ * event keeps its identity through dispatch, as in the DOM). */
77
+ xt_object_set(event, xt_string_from_cstr("__xt_gui_event"), XT_TRUE);
51
78
  return event;
52
79
  }
53
80
 
@@ -64,18 +91,32 @@ void remove_listener(XtGuiWindow *win, xtgui::Node *node, const std::string &typ
64
91
  }
65
92
  }
66
93
 
94
+ /** Call one listener batch. `phase` mirrors the DOM constants (1 capture,
95
+ * 2 at target, 3 bubble) so `event.eventPhase` is observable, and a passive
96
+ * listener runs with `preventDefault()` disabled. */
67
97
  static bool call_node_listeners(XtGuiWindow *win, xtgui::Node *node, const char *type,
68
- xt_value event, bool capture) {
98
+ xt_value event, bool capture, int32_t phase) {
69
99
  auto found = win->node_listeners.find(node);
70
100
  if (found == win->node_listeners.end()) return false;
71
101
  std::vector<XtGuiNodeListener> snapshot = found->second;
102
+ xt_value previousPassive = xt_object_get_cstr(event, "__xt_gui_passive");
72
103
  for (const XtGuiNodeListener &listener : snapshot) {
73
104
  if (listener.type != type || listener.capture != capture) continue;
105
+ /* An aborted `AbortSignal` removes the listener. Drop it here so it also
106
+ * stops consuming the slot for future dispatches. */
107
+ if (xt_gui_signal_aborted(listener.signal)) {
108
+ remove_listener(win, node, listener.type, listener.fn, listener.capture);
109
+ continue;
110
+ }
74
111
  if (listener.once) remove_listener(win, node, listener.type, listener.fn, listener.capture);
75
112
  xt_value self = xt_gui_node_handle(win, node);
76
113
  xt_object_set(event, xt_string_from_cstr("currentTarget"), self);
114
+ xt_object_set(event, xt_string_from_cstr("eventPhase"), xt_number((double)phase));
115
+ xt_object_set(event, xt_string_from_cstr("__xt_gui_passive"),
116
+ listener.passive ? XT_TRUE : XT_FALSE);
77
117
  xt_value args[1] = {event};
78
118
  xt_call_with_this(listener.fn, self, 1, args);
119
+ xt_object_set(event, xt_string_from_cstr("__xt_gui_passive"), previousPassive);
79
120
  if (value_truthy(xt_object_get_cstr(event, "__stopImmediate"))) return true;
80
121
  if (value_truthy(xt_object_get_cstr(event, "__stop"))) return true;
81
122
  }
@@ -88,6 +129,9 @@ METHOD(event_prevent_default) {
88
129
  (void)env;
89
130
  (void)argc;
90
131
  (void)argv;
132
+ /* A listener registered with `{ passive: true }` promised not to cancel: the
133
+ * browser ignores the call (and warns), so the engine ignores it too. */
134
+ if (value_truthy(xt_object_get_cstr(self, "__xt_gui_passive"))) return XT_UNDEFINED;
91
135
  xt_object_set(self, xt_string_from_cstr("defaultPrevented"), XT_TRUE);
92
136
  return XT_UNDEFINED;
93
137
  }
@@ -109,6 +153,22 @@ METHOD(event_stop_immediate_propagation) {
109
153
  return XT_UNDEFINED;
110
154
  }
111
155
 
156
+ /** `event.composedPath()`: the propagation path recorded by the dispatcher,
157
+ * target first, as element handles. A detached event has an empty path. */
158
+ METHOD(event_composed_path) {
159
+ (void)env;
160
+ (void)argc;
161
+ (void)argv;
162
+ xt_value result = xt_array_new(0, nullptr);
163
+ xt_value path = xt_object_get_cstr(self, "__xt_gui_path");
164
+ if (!XT_IS_ARRAY(path)) return result;
165
+ int32_t count = (int32_t)xt_to_number(xt_array_length(path));
166
+ for (int32_t i = 0; i < count; i++) {
167
+ xt_array_push(result, xt_array_get(path, xt_number((double)i)));
168
+ }
169
+ return result;
170
+ }
171
+
112
172
  #undef METHOD
113
173
 
114
174
  } // namespace domapi
@@ -123,13 +183,29 @@ void xt_gui_emit_dom_event(XtGuiWindow *win, const char *type, const xtgui::Node
123
183
  path.push_back(node);
124
184
  }
125
185
  xt_value event = make_dom_event(win, type, target, legacy);
186
+ /* `element.dispatchEvent(ev)` hands over a real event object: dispatch that
187
+ * same object (identity is observable), only rebinding its target/path. */
188
+ if (XT_IS_OBJECT(legacy) && value_truthy(xt_object_get_cstr(legacy, "__xt_gui_event"))) {
189
+ event = legacy;
190
+ xt_object_set(event, xt_string_from_cstr("target"), xt_gui_node_handle(win, target));
191
+ xt_object_set(event, xt_string_from_cstr("currentTarget"), XT_NULL);
192
+ xt_object_set(event, xt_string_from_cstr("eventPhase"), xt_number(0));
193
+ }
194
+ xt_value handlePath = xt_array_new(0, nullptr);
195
+ for (xtgui::Node *node : path) xt_array_push(handlePath, xt_gui_node_handle(win, node));
196
+ xt_object_set(event, xt_string_from_cstr("__xt_gui_path"), handlePath);
197
+ bool bubbles = value_truthy(xt_object_get_cstr(event, "bubbles"));
126
198
  /* Capture phase: root -> target. */
127
199
  for (auto it = path.rbegin(); it != path.rend(); ++it) {
128
- if (call_node_listeners(win, *it, type, event, true)) return;
200
+ int32_t phase = *it == target ? 2 : 1;
201
+ if (call_node_listeners(win, *it, type, event, true, phase)) return;
129
202
  }
130
- /* Bubble phase: target -> root. */
203
+ /* Bubble phase: target -> root; a non-bubbling event stops after the target
204
+ * (its non-capture listeners still run there). */
131
205
  for (auto it = path.begin(); it != path.end(); ++it) {
132
- if (call_node_listeners(win, *it, type, event, false)) return;
206
+ if (!bubbles && *it != target) break;
207
+ int32_t phase = *it == target ? 2 : 3;
208
+ if (call_node_listeners(win, *it, type, event, false, phase)) return;
133
209
  }
134
210
  }
135
211
 
@@ -27,6 +27,7 @@ extern xt_value g_document_proto;
27
27
  extern xt_value g_event_proto;
28
28
  extern xt_value g_class_list_proto;
29
29
  extern xt_value g_style_proto;
30
+ extern xt_value g_computed_style_proto;
30
31
  extern xt_value g_canvas_proto;
31
32
  extern xt_value g_canvas_gradient_proto;
32
33
  extern xt_value g_webgl_proto;
@@ -49,10 +50,16 @@ xt_value document_proto();
49
50
  xt_value event_proto();
50
51
  xt_value class_list_proto();
51
52
  xt_value style_proto();
53
+ xt_value computed_style_proto();
52
54
  xt_value canvas_proto();
53
55
  xt_value canvas_gradient_proto();
54
56
  xt_value webgl_proto();
55
57
  xt_value webgl2_proto();
58
+ /** `AbortController` / `AbortSignal` prototypes and constructors. */
59
+ xt_value abort_controller_proto();
60
+ xt_value abort_signal_proto();
61
+ xt_value abort_controller_new();
62
+ xt_value abort_signal_new(bool aborted);
56
63
  /** The prototype a WebGL object of `kind` inherits (`buffer`, `texture`, …),
57
64
  * so `buffer instanceof WebGLBuffer` holds. */
58
65
  xt_value webgl_object_proto(const std::string &kind);
@@ -60,6 +67,16 @@ xt_value webgl_object_proto(const std::string &kind);
60
67
  /** Create/return the 2D context for a pending `<canvas>` handle (before load). */
61
68
  xt_value xt_canvas_pending_context(xt_value self, XtGuiWindow *win);
62
69
 
70
+ /* -- Path2D --------------------------------------------------------------- */
71
+
72
+ /** True when `value` is a `Path2D` handle. */
73
+ bool path2d_is(xt_value value);
74
+ /** The user-space subpaths a `Path2D` handle holds. */
75
+ std::vector<xtgui::CanvasSubpath> path2d_subpaths(xt_value value);
76
+ /** `new Path2D()` / `new Path2D(other)`. */
77
+ xt_value path2d_create(xt_value source);
78
+ xt_value path2d_proto();
79
+
63
80
  /* -- canvas helpers shared by `dom_api_canvas*.cpp` ----------------------- */
64
81
 
65
82
  /** The engine canvas behind a 2D context handle, or null when stale/foreign. */
@@ -157,7 +174,9 @@ METHOD(doc_create_element_ns);
157
174
  METHOD(doc_hidden_get);
158
175
  METHOD(doc_visibility_state_get);
159
176
  METHOD(doc_ready_state_get);
177
+ METHOD(doc_has_focus);
160
178
  METHOD(doc_title_get);
179
+ METHOD(doc_title_set);
161
180
  METHOD(doc_default_view_get);
162
181
  METHOD(doc_create_text_node);
163
182
  METHOD(doc_body_get);
@@ -166,6 +185,9 @@ METHOD(doc_document_element_get);
166
185
  /* node getters */
167
186
  METHOD(node_tag_name_get);
168
187
  METHOD(node_node_type_get);
188
+ METHOD(node_namespace_uri_get);
189
+ METHOD(node_local_name_get);
190
+ METHOD(node_prefix_get);
169
191
  METHOD(node_id_get);
170
192
  METHOD(node_id_set);
171
193
  METHOD(node_class_name_get);
@@ -196,6 +218,10 @@ METHOD(node_query_selector);
196
218
  METHOD(node_query_selector_all);
197
219
  METHOD(node_matches);
198
220
  METHOD(node_append_child);
221
+ METHOD(node_append);
222
+ METHOD(node_prepend);
223
+ METHOD(node_replace_children);
224
+ METHOD(node_insert_adjacent_html);
199
225
  METHOD(node_insert_before);
200
226
  METHOD(node_remove_child);
201
227
  METHOD(node_replace_child);
@@ -219,6 +245,7 @@ xt_value event_construct(xt_value type, xt_value init);
219
245
  METHOD(event_prevent_default);
220
246
  METHOD(event_stop_propagation);
221
247
  METHOD(event_stop_immediate_propagation);
248
+ METHOD(event_composed_path);
222
249
 
223
250
  /* media (`<video>`/`<audio>`) */
224
251
  METHOD(node_play);
@@ -285,11 +312,21 @@ METHOD(canvas_stroke);
285
312
  METHOD(canvas_clear_rect);
286
313
  METHOD(canvas_fill_rect);
287
314
  METHOD(canvas_stroke_rect);
288
- METHOD(canvas_measure_text);
289
- METHOD(canvas_fill_text);
315
+ METHOD(canvas_measure_text);METHOD(canvas_fill_text);
290
316
  METHOD(canvas_stroke_text);
291
317
  METHOD(canvas_draw_image);
292
318
  METHOD(canvas_get_image_data);
319
+ METHOD(canvas_get_context_attributes);
320
+ METHOD(canvas_put_image_data);
321
+ METHOD(canvas_create_image_data);
322
+ METHOD(canvas_create_pattern);
323
+ METHOD(canvas_to_data_url);
324
+ METHOD(canvas_to_blob);
325
+ /** The cached `CanvasPattern` handle for `id` on `canvas`. */
326
+ xt_value canvas_pattern_handle(xt_value context, const xtgui::Canvas2D *canvas, uint64_t id);
327
+ /** The pattern id stored on a handle, or 0 when it is not a pattern for
328
+ * `canvas` (stale or created by another canvas). */
329
+ uint64_t canvas_pattern_id(xt_value handle, const xtgui::Canvas2D *canvas);
293
330
  METHOD(canvas_ellipse);
294
331
  METHOD(canvas_round_rect);
295
332
  METHOD(canvas_line_cap_get);
@@ -316,7 +353,23 @@ METHOD(class_list_contains);
316
353
  /* style methods */
317
354
  METHOD(style_set_property);
318
355
  METHOD(style_get_property_value);
356
+ METHOD(style_get_property_priority);
319
357
  METHOD(style_remove_property);
358
+ METHOD(style_css_text_get);
359
+ METHOD(style_css_text_set);
360
+ METHOD(style_length_get);
361
+ METHOD(style_item);
362
+ /** Install the dynamic named-property hooks on a `style` handle
363
+ * (`style.width = "50%"`). */
364
+ void install_style_hooks(xt_value style);
365
+
366
+ /* `dataset` (a DOMStringMap over `data-*` attributes) and computed style */
367
+ METHOD(node_dataset_get);
368
+ METHOD(computed_style_get_property_value);
369
+ METHOD(computed_style_get_property_priority);
370
+ /** Build the computed-style handle for an element (and optional pseudo). */
371
+ xt_value computed_style_handle(xt_value element, xt_value pseudo);
372
+ xt_value computed_style_proto();
320
373
 
321
374
  /* WebGL context: state and queries */
322
375
  METHOD(webgl_canvas_get);
@@ -0,0 +1,159 @@
1
+ /*
2
+ * xbintsc GUI engine — the parent/child convenience mutators.
3
+ *
4
+ * `append`, `prepend`, `replaceChildren` and `insertAdjacentHTML` are the APIs
5
+ * modern pages actually use to build DOM (they accept nodes and plain strings
6
+ * in one call); the engine previously only had `appendChild`/`innerHTML`.
7
+ * Strings become text nodes, as in the DOM.
8
+ */
9
+
10
+ #include "dom_api_internal.h"
11
+
12
+ #include <memory>
13
+ #include <string>
14
+ #include <vector>
15
+
16
+ namespace domapi {
17
+
18
+ namespace {
19
+
20
+ /** A node handle, or a new text node for a string argument. */
21
+ xtgui::Node *argument_node(XtGuiWindow *win, xt_value value) {
22
+ xtgui::Node *node = resolve_node(value, nullptr);
23
+ if (node != nullptr) return node;
24
+ if (XT_IS_UNDEFINED(value) || value == XT_NULL) return nullptr;
25
+ return win->document->createTextNode(to_string_value(value));
26
+ }
27
+
28
+ void insert_argument(XtGuiWindow *win, xtgui::Node *parent, xt_value value, xtgui::Node *before) {
29
+ xtgui::Node *node = argument_node(win, value);
30
+ if (node == nullptr) return;
31
+ if (before != nullptr) {
32
+ win->document->insertBefore(parent, node, before);
33
+ } else {
34
+ win->document->appendChild(parent, node);
35
+ }
36
+ }
37
+
38
+ /** Copy a parsed fragment's children into `dest`, inserting before `before`
39
+ * (nullptr appends). The parser's nodes belong to a standalone tree, so they are
40
+ * recreated through the document rather than adopted. */
41
+ void import_children(XtGuiWindow *win, xtgui::Node *dest, const xtgui::Node *parsed,
42
+ xtgui::Node *before) {
43
+ for (const std::unique_ptr<xtgui::Node> &child : parsed->children) {
44
+ xtgui::Node *copy = nullptr;
45
+ if (child->isText()) {
46
+ copy = win->document->createTextNode(child->text);
47
+ } else {
48
+ copy = win->document->createElement(child->tag);
49
+ if (copy == nullptr) continue;
50
+ for (const xtgui::Attr &attr : child->attrs) copy->attrs.push_back(attr);
51
+ import_children(win, copy, child.get(), nullptr);
52
+ }
53
+ if (copy == nullptr) continue;
54
+ if (before != nullptr) {
55
+ win->document->insertBefore(dest, copy, before);
56
+ } else {
57
+ win->document->appendChild(dest, copy);
58
+ }
59
+ }
60
+ }
61
+
62
+ xtgui::Node *next_sibling(xtgui::Node *parent, xtgui::Node *node) {
63
+ if (parent == nullptr) return nullptr;
64
+ for (size_t i = 0; i + 1 < parent->children.size(); i++) {
65
+ if (parent->children[i].get() == node) return parent->children[i + 1].get();
66
+ }
67
+ return nullptr;
68
+ }
69
+
70
+ xtgui::Node *first_child(xtgui::Node *node) {
71
+ return node->children.empty() ? nullptr : node->children.front().get();
72
+ }
73
+
74
+ } // namespace
75
+
76
+ #define METHOD(name) xt_value name(xt_value self, xt_value env, int32_t argc, xt_value *argv)
77
+
78
+ METHOD(node_append) {
79
+ (void)env;
80
+ XtGuiWindow *win = nullptr;
81
+ xtgui::Node *node = resolve_node(self, &win);
82
+ if (node == nullptr || win == nullptr) return XT_UNDEFINED;
83
+ for (int32_t i = 0; i < argc; i++) insert_argument(win, node, xt_arg(argc, argv, i), nullptr);
84
+ mark_dirty(win);
85
+ return XT_UNDEFINED;
86
+ }
87
+
88
+ METHOD(node_prepend) {
89
+ (void)env;
90
+ XtGuiWindow *win = nullptr;
91
+ xtgui::Node *node = resolve_node(self, &win);
92
+ if (node == nullptr || win == nullptr) return XT_UNDEFINED;
93
+ /* The reference stays the original first child; inserting each argument before
94
+ * it keeps the argument order. */
95
+ xtgui::Node *reference = first_child(node);
96
+ for (int32_t i = 0; i < argc; i++) {
97
+ insert_argument(win, node, xt_arg(argc, argv, i), reference);
98
+ }
99
+ mark_dirty(win);
100
+ return XT_UNDEFINED;
101
+ }
102
+
103
+ METHOD(node_replace_children) {
104
+ (void)env;
105
+ XtGuiWindow *win = nullptr;
106
+ xtgui::Node *node = resolve_node(self, &win);
107
+ if (node == nullptr || win == nullptr || win->document == nullptr) return XT_UNDEFINED;
108
+ /* Detach node arguments first: one of them may be a current child that the
109
+ * replacement keeps. */
110
+ for (int32_t i = 0; i < argc; i++) {
111
+ xtgui::Node *child = resolve_node(xt_arg(argc, argv, i), nullptr);
112
+ if (child == nullptr) continue;
113
+ if (child->parent == node) win->document->removeChild(node, child);
114
+ }
115
+ std::vector<xtgui::Node *> existing;
116
+ for (const std::unique_ptr<xtgui::Node> &child : node->children) {
117
+ existing.push_back(child.get());
118
+ }
119
+ for (xtgui::Node *child : existing) {
120
+ win->document->removeChild(node, child);
121
+ discard_subtree(win, child);
122
+ }
123
+ for (int32_t i = 0; i < argc; i++) insert_argument(win, node, xt_arg(argc, argv, i), nullptr);
124
+ mark_dirty(win);
125
+ return XT_UNDEFINED;
126
+ }
127
+
128
+ /** `insertAdjacentHTML(position, html)`; `position` is beforebegin,
129
+ * afterbegin, beforeend or afterend. Scripts in the fragment do not run. */
130
+ METHOD(node_insert_adjacent_html) {
131
+ (void)env;
132
+ XtGuiWindow *win = nullptr;
133
+ xtgui::Node *node = resolve_node(self, &win);
134
+ if (node == nullptr || win == nullptr || win->document == nullptr || argc < 2) {
135
+ return XT_UNDEFINED;
136
+ }
137
+ std::string position = to_string_value(xt_arg(argc, argv, 0));
138
+ std::string html = to_string_value(xt_arg(argc, argv, 1));
139
+ std::unique_ptr<xtgui::Node> parsed = xtgui::xt_html_parse(html);
140
+ if (!parsed) return XT_UNDEFINED;
141
+ if (position == "beforebegin" || position == "afterend") {
142
+ xtgui::Node *parent = node->parent;
143
+ if (parent == nullptr || parent->isDocument()) return XT_UNDEFINED;
144
+ xtgui::Node *reference = position == "beforebegin" ? node : next_sibling(parent, node);
145
+ import_children(win, parent, parsed.get(), reference);
146
+ } else if (position == "afterbegin") {
147
+ import_children(win, node, parsed.get(), first_child(node));
148
+ } else if (position == "beforeend") {
149
+ import_children(win, node, parsed.get(), nullptr);
150
+ } else {
151
+ return XT_UNDEFINED;
152
+ }
153
+ mark_dirty(win);
154
+ return XT_UNDEFINED;
155
+ }
156
+
157
+ #undef METHOD
158
+
159
+ } // namespace domapi
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  #include "dom_api_internal.h"
7
+ #include "listener_options.h"
7
8
 
8
9
  #include <algorithm>
9
10
  #include <cmath>
@@ -217,13 +218,19 @@ METHOD(node_get_bounding_rect) {
217
218
  if (node == nullptr || win == nullptr || win->document == nullptr) return XT_UNDEFINED;
218
219
  const xtgui::LayoutBox *box = win->document->boxOf(node);
219
220
  if (box == nullptr) return XT_UNDEFINED;
220
- /* Like the DOM: the rectangle of the *transformed* border box. */
221
+ /* Like the DOM: the rectangle of the *transformed* border box. The legacy
222
+ * `top`/`right`/`bottom`/`left` members are the same rectangle in the other
223
+ * spelling; libraries read both. */
221
224
  xtgui::LayoutRect rect = xtgui::xt_layout_transformed_bounds(box);
222
225
  xt_value object = xt_object_new();
223
226
  xt_object_set(object, xt_string_from_cstr("x"), xt_number(rect.x));
224
227
  xt_object_set(object, xt_string_from_cstr("y"), xt_number(rect.y));
225
228
  xt_object_set(object, xt_string_from_cstr("width"), xt_number(rect.width));
226
229
  xt_object_set(object, xt_string_from_cstr("height"), xt_number(rect.height));
230
+ xt_object_set(object, xt_string_from_cstr("top"), xt_number(rect.y));
231
+ xt_object_set(object, xt_string_from_cstr("right"), xt_number(rect.x + rect.width));
232
+ xt_object_set(object, xt_string_from_cstr("bottom"), xt_number(rect.y + rect.height));
233
+ xt_object_set(object, xt_string_from_cstr("left"), xt_number(rect.x));
227
234
  return object;
228
235
  }
229
236
 
@@ -252,12 +259,30 @@ METHOD(node_contains) {
252
259
 
253
260
  METHOD(node_focus) {
254
261
  (void)env;
255
- (void)argc;
256
- (void)argv;
257
262
  XtGuiWindow *win = nullptr;
258
263
  xtgui::Node *node = resolve_node(self, &win);
259
264
  if (node == nullptr || win == nullptr || win->document == nullptr) return XT_UNDEFINED;
265
+ /* `{ preventScroll: true }` keeps the current page offset; otherwise the
266
+ * focused box is scrolled into view, like the browser default. */
267
+ bool prevent_scroll = false;
268
+ xt_value options = xt_arg(argc, argv, 0);
269
+ if (XT_IS_OBJECT(options)) {
270
+ prevent_scroll = value_truthy(xt_object_get_cstr(options, "preventScroll"));
271
+ }
260
272
  if (win->document->setFocus(node)) win->geometry.dirty = 1;
273
+ const xtgui::LayoutBox *box = win->document->boxOf(node);
274
+ if (!prevent_scroll && box != nullptr) {
275
+ float viewport_width = 0;
276
+ float viewport_height = 0;
277
+ xt_gui_window_viewport(win, &viewport_width, &viewport_height);
278
+ float x = win->scroll_x;
279
+ float y = win->scroll_y;
280
+ if (box->x < x) x = box->x;
281
+ else if (box->x + box->width > x + viewport_width) x = box->x + box->width - viewport_width;
282
+ if (box->y < y) y = box->y;
283
+ else if (box->y + box->height > y + viewport_height) y = box->y + box->height - viewport_height;
284
+ xt_gui_scroll_to(win, x, y);
285
+ }
261
286
  return XT_UNDEFINED;
262
287
  }
263
288
 
@@ -290,20 +315,18 @@ METHOD(node_add_event_listener) {
290
315
  if (node == nullptr || win == nullptr || argc < 2) return self;
291
316
  xt_value fn = xt_arg(argc, argv, 1);
292
317
  if (!XT_IS_FUNCTION(fn)) return self;
293
- bool capture = false;
294
- bool once = false;
295
- xt_value options = xt_arg(argc, argv, 2);
296
- if (XT_IS_OBJECT(options)) {
297
- capture = value_truthy(xt_object_get_cstr(options, "capture"));
298
- once = value_truthy(xt_object_get_cstr(options, "once"));
299
- } else if (XT_IS_BOOL(options)) {
300
- capture = value_truthy(options);
301
- }
318
+ XtGuiListenerOptions options = xt_gui_listener_options(xt_arg(argc, argv, 2));
319
+ /* `{ signal: controller.signal }` removes the listener: an already-aborted
320
+ * signal means it was never installed, and a later abort is honoured at
321
+ * dispatch time (see `call_node_listeners`). */
322
+ if (xt_gui_signal_aborted(options.signal)) return self;
302
323
  XtGuiNodeListener listener;
303
324
  listener.type = to_string_value(xt_arg(argc, argv, 0));
304
325
  listener.fn = fn;
305
- listener.once = once;
306
- listener.capture = capture;
326
+ listener.once = options.once;
327
+ listener.capture = options.capture;
328
+ listener.passive = options.passive;
329
+ listener.signal = options.signal;
307
330
  win->node_listeners[node].push_back(listener);
308
331
  return self;
309
332
  }
@@ -313,11 +336,9 @@ METHOD(node_remove_event_listener) {
313
336
  XtGuiWindow *win = nullptr;
314
337
  xtgui::Node *node = resolve_node(self, &win);
315
338
  if (node == nullptr || win == nullptr || argc < 2) return self;
316
- bool capture = false;
317
- xt_value options = xt_arg(argc, argv, 2);
318
- if (XT_IS_OBJECT(options)) capture = value_truthy(xt_object_get_cstr(options, "capture"));
319
- else if (XT_IS_BOOL(options)) capture = value_truthy(options);
320
- remove_listener(win, node, to_string_value(xt_arg(argc, argv, 0)), xt_arg(argc, argv, 1), capture);
339
+ XtGuiListenerOptions options = xt_gui_listener_options(xt_arg(argc, argv, 2));
340
+ remove_listener(win, node, to_string_value(xt_arg(argc, argv, 0)), xt_arg(argc, argv, 1),
341
+ options.capture);
321
342
  return self;
322
343
  }
323
344