xbintsc 0.5.42 → 0.5.45

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 (43) 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/tests/e2e/gui-audio.test.js +22 -0
  5. package/dist/tests/e2e/gui-audio.test.js.map +1 -1
  6. package/dist/tests/e2e/gui-browser.test.js +3 -0
  7. package/dist/tests/e2e/gui-browser.test.js.map +1 -1
  8. package/dist/tests/e2e/gui-effects.test.js +48 -4
  9. package/dist/tests/e2e/gui-effects.test.js.map +1 -1
  10. package/dist/tests/e2e/gui-events.test.js +67 -0
  11. package/dist/tests/e2e/gui-events.test.js.map +1 -1
  12. package/doc/ai/extensions.md +5 -3
  13. package/doc/ai/zh-CN/extensions.md +4 -3
  14. package/doc/gui/api.md +13 -5
  15. package/doc/gui/input.md +7 -0
  16. package/doc/gui/known-issues.md +5 -3
  17. package/doc/gui/rendering.md +12 -8
  18. package/doc/gui-scripts.md +2 -1
  19. package/doc/gui.md +7 -4
  20. package/doc/zh-CN/gui/api.md +12 -5
  21. package/doc/zh-CN/gui/input.md +5 -0
  22. package/doc/zh-CN/gui/known-issues.md +3 -2
  23. package/doc/zh-CN/gui/rendering.md +9 -6
  24. package/doc/zh-CN/gui-scripts.md +2 -2
  25. package/doc/zh-CN/gui.md +6 -5
  26. package/package.json +7 -7
  27. package/runtime/ext_gui/audio.cpp +18 -0
  28. package/runtime/ext_gui/audio_device.cpp +12 -2
  29. package/runtime/ext_gui/browser.cpp +2 -2
  30. package/runtime/ext_gui/dom_api_events.cpp +2 -1
  31. package/runtime/ext_gui/gui.cpp +7 -0
  32. package/runtime/ext_gui/gui_engine.h +9 -0
  33. package/runtime/ext_gui/gui_events.cpp +52 -10
  34. package/runtime/ext_gui/gui_keymap.cpp +254 -0
  35. package/runtime/ext_gui/gui_keymap.h +31 -0
  36. package/runtime/ext_gui/paint.cpp +16 -1
  37. package/runtime/ext_gui/paint.h +46 -0
  38. package/runtime/ext_gui/paint_background.cpp +139 -64
  39. package/runtime/ext_gui/renderer_gradient.cpp +156 -0
  40. package/runtime/ext_gui/renderer_image.cpp +6 -1
  41. package/runtime/ext_gui/renderer_internal.h +22 -0
  42. package/runtime/ext_gui/window.cpp +10 -1
  43. package/src/driver/version.ts +1 -1
@@ -72,18 +72,21 @@
72
72
  `to <side>`、`deg`/`grad`/`rad`/`turn`、百分比或长度(`px`/`em`/…)偏移、
73
73
  `color <start> <end>` 简写与多色标(色带沿最近轴行走,因此斜角会退化为
74
74
  最近的轴向);
75
- - **径向**渐变是 32 个同心椭圆(重复渐变 64 个)。每个都是单位半径的圆角矩形,
76
- 经图形管线的 transform 槽映射,因此圆/椭圆仍保留距离场抗锯齿。支持形状/尺寸
77
- 关键字(`circle`/`ellipse`、`closest-side`/`farthest-side`/`closest-corner`/
78
- `farthest-corner`、显式半径)与 `at <position>`。
75
+ - **径向**渐变由 `renderer_gradient.cpp` 逐像素解算成一张 RGBA 纹理,作为单个
76
+ 图像四边形绘制(`PaintGradient`)。若沿用嵌套色带,由于每条色带都会覆盖整个
77
+ 内部区域,alpha 会叠加多次——淡出到 `transparent` 的暗角会把整个场景涂成实心
78
+ 黑盘。支持形状/尺寸关键字(`circle`/`ellipse`、`closest-side`/`farthest-side`/
79
+ `closest-corner`/`farthest-corner`、显式半径)与 `at <position>`;关键字尺寸
80
+ 的椭圆保持盒子的宽高比并缩放到刚好接触对应边或角,与 CSS 一致。色标在预乘
81
+ alpha 空间中插值,最后一个色标会延伸覆盖结束形状之外的剩余盒子。
79
82
 
80
83
  `repeating-linear-gradient()` 与 `repeating-radial-gradient()` 会在整个盒子上
81
84
  从 0 平铺到最后一个色标的偏移量;色标列表不会被强制结束在 100%,因此
82
85
  `red 0%, blue 10%` 每 10% 重复一次。普通渐变的最后一个色标若小于 100%,则剩余
83
86
  区域保持该颜色,与 CSS 一致。
84
87
 
85
- 背景色位于所有层下方。若渐变色标带透明度,渐变也会半透明;色带近似可能产生
86
- 轻微条带,非常透明的多色标渐变会出现 alpha 叠加。
88
+ 背景色位于所有层下方。若渐变色标带透明度,渐变也会半透明;径向渐变逐像素精确,
89
+ 线性渐变仍采样 18 条色带,较大的柔和渐变可能出现条带。
87
90
 
88
91
  **生成内容**不需要新的显示列表类型:`::before`/`::after` 是 `style.cpp` 中
89
92
  每次重算样式时重建的普通布局盒,因此其背景、边框、文本与 transform 都走同一条
@@ -314,8 +314,8 @@ Event 的 `target` 是句柄,其描述符(`toString`)与同一 `div#id.cla
314
314
  导入的资产会)。如有需要,M9d 会处理。
315
315
  - **外部 `<script src>` 文件之间的 import 绑定冲突。** 会被检测并报告
316
316
  (给 import 起别名);之后可以做到带引用重写的按脚本重命名。
317
- - **资产。** `<script src>` 相对 HTML 资产解析;URL `src` 值被忽略。没有
318
- fetch/网络加载。
317
+ - **资产。** `<script src>` 相对 HTML 资产解析;URL `src` 值被忽略。
318
+ 加载器不会请求网络 URL;运行时全局 `fetch` 是另一套 API。
319
319
 
320
320
  ## 测试
321
321
 
package/doc/zh-CN/gui.md CHANGED
@@ -262,8 +262,8 @@ FFmpeg `7.1`(供可选的 `media` 归档使用;可用 `SDL3_TAG` /
262
262
  特性);`matchMedia()` 使用同一个求值器。
263
263
  - 生成内容:`::before`/`::after` 选择器、继承的伪样式、`content` 字符串/
264
264
  转义/`attr()`,以及不会泄漏到 DOM 遍历的合成布局盒。
265
- - 多层 `background-image` 的线性与径向渐变,自下而上按纯色带绘制
266
- (线性 18 条、径向 32 个同心椭圆)。
265
+ - 多层 `background-image`:线性渐变自下而上按纯色带绘制(18 条),径向渐变
266
+ 逐像素光栅化成一张图像纹理(`renderer_gradient.cpp`)。
267
267
  - `text-shadow` 作为阴影文本 run,并带九次采样模糊近似。
268
268
  - `clip-path` 多边形(`inset`/`circle`/`ellipse`/`polygon`)解析到设备空间,
269
269
  用耳切法三角化,并在 `renderer_clip.cpp` 中与图形几何求交——不需要模板/
@@ -408,9 +408,10 @@ FFmpeg `7.1`(供可选的 `media` 归档使用;可用 `SDL3_TAG` /
408
408
  识别 `::before`/`::after`。`style.cpp` 把伪样式算进每次重算样式时重建的
409
409
  arena、解析 `content`,布局在真实子元素前后插入它们的盒子。
410
410
  `gradient_values.cpp` 建模背景层列表(线性/径向、形状/尺寸/位置/色标),
411
- `paint_background.cpp` 绘制它,`paint_effects.cpp` 应用滤镜颜色函数与九次
412
- 采样模糊/投影并生成 `text-shadow` run,`paint_clip.cpp`/`renderer_clip.cpp`
413
- 对三角化的 `clip-path` 多边形裁剪几何。e2e 见
411
+ `paint_background.cpp` 解算并绘制它(径向层变成一个 `PaintGradient`,由
412
+ `renderer_gradient.cpp` 光栅化成纹理),`paint_effects.cpp` 应用滤镜颜色函数
413
+ 与九次采样模糊/投影并生成 `text-shadow` run,`paint_clip.cpp`/
414
+ `renderer_clip.cpp` 对三角化的 `clip-path` 多边形裁剪几何。e2e 见
414
415
  `tests/e2e/gui-effects.test.ts`。
415
416
  - **M20** ✅ 渲染性能。`@media` 每次重算样式时每条规则只求值一次;没有
416
417
  `::before`/`::after` 的样式表完全跳过生成内容;只有样式表包含
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xbintsc",
3
- "version": "0.5.42",
3
+ "version": "0.5.45",
4
4
  "description": "xbintsc - a TypeScript binary compiler (TypeScript -> LLVM IR -> native binary)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -72,12 +72,12 @@
72
72
  "vitest": "^2.1.8"
73
73
  },
74
74
  "optionalDependencies": {
75
- "@xbintsc/linux-x64": "0.5.42",
76
- "@xbintsc/linux-arm64": "0.5.42",
77
- "@xbintsc/darwin-x64": "0.5.42",
78
- "@xbintsc/darwin-arm64": "0.5.42",
79
- "@xbintsc/win32-x64": "0.5.42",
80
- "@xbintsc/win32-arm64": "0.5.42"
75
+ "@xbintsc/linux-x64": "0.5.45",
76
+ "@xbintsc/linux-arm64": "0.5.45",
77
+ "@xbintsc/darwin-x64": "0.5.45",
78
+ "@xbintsc/darwin-arm64": "0.5.45",
79
+ "@xbintsc/win32-x64": "0.5.45",
80
+ "@xbintsc/win32-arm64": "0.5.45"
81
81
  },
82
82
  "engines": {
83
83
  "node": ">=22"
@@ -500,6 +500,24 @@ uint64_t audio_context_id(xt_value object) {
500
500
 
501
501
  xt_value xt_gui_audio_proto(void) { return audio_proto(); }
502
502
 
503
+ /** Close every live context before `SDL_Quit`. The mixers hold SDL audio
504
+ * streams whose destructors must not run after the audio subsystem (and its
505
+ * device objects) were torn down; keeping the registry alive until static
506
+ * destruction was a crash on exit for every program that played a sound. */
507
+ void xt_gui_audio_shutdown(void) {
508
+ std::vector<std::shared_ptr<AudioMixer>> mixers;
509
+ {
510
+ std::lock_guard<std::mutex> lock(g_registry_mutex);
511
+ mixers.reserve(g_mixers.size());
512
+ for (const auto &entry : g_mixers) mixers.push_back(entry.second);
513
+ g_mixers.clear();
514
+ }
515
+ for (const std::shared_ptr<AudioMixer> &mixer : mixers) {
516
+ mixer->close_device();
517
+ mixer->shutdown();
518
+ }
519
+ }
520
+
503
521
  xt_value xt_gui_audio_create(int32_t argc, xt_value *argv) {
504
522
  bool offline = false;
505
523
  xt_value options = xt_arg(argc, argv, 0);
@@ -13,6 +13,7 @@
13
13
 
14
14
  #include <SDL3/SDL.h>
15
15
 
16
+ #include <cstdio>
16
17
  #include <vector>
17
18
 
18
19
  namespace xtgui {
@@ -39,14 +40,23 @@ bool AudioMixer::start_device() {
39
40
  if (stream_ != nullptr) return true;
40
41
  if (device_tried_) return false;
41
42
  device_tried_ = true;
42
- if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) return false;
43
+ if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
44
+ /* A missing device is not fatal (the page keeps its graph and clock), but
45
+ * silently staying quiet is indistinguishable from a broken mixer. */
46
+ std::fprintf(stderr, "xt_gui: audio subsystem unavailable: %s\n", SDL_GetError());
47
+ return false;
48
+ }
43
49
  SDL_AudioSpec spec;
44
50
  spec.format = SDL_AUDIO_F32;
45
51
  spec.channels = kAudioMixChannels;
46
52
  spec.freq = kAudioMixRate;
47
53
  stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, stream_callback,
48
54
  this);
49
- if (stream_ == nullptr) return false;
55
+ if (stream_ == nullptr) {
56
+ std::fprintf(stderr, "xt_gui: could not open the default audio device: %s\n",
57
+ SDL_GetError());
58
+ return false;
59
+ }
50
60
  SDL_ResumeAudioStreamDevice(stream_);
51
61
  return true;
52
62
  }
@@ -393,7 +393,7 @@ xt_value host_type_construct(xt_value self, xt_value env, int32_t argc, xt_value
393
393
  /** Host constructors are window members too — a page writes
394
394
  * `new window.AudioContext()` as often as `new AudioContext()`. */
395
395
  void install_window_globals(xt_value win) {
396
- if (xt_seq(win, g_installed_window)) return;
396
+ if (xt_truthy(xt_seq(win, g_installed_window))) return;
397
397
  g_installed_window = win;
398
398
  xt_gc_add_root(&g_installed_window);
399
399
  if (XT_IS_OBJECT(g_navigator)) set_prop(win, "navigator", g_navigator);
@@ -479,7 +479,7 @@ xt_value xt_browser_document(int32_t argc, xt_value *argv) {
479
479
  xt_value win = xt_browser_window(0, NULL);
480
480
  if (XT_IS_UNDEFINED(win)) return XT_UNDEFINED;
481
481
  xt_value doc = xt_gui_document_handle(xt_gui_window_from_this(win));
482
- if (!xt_seq(doc, g_document_global)) {
482
+ if (!xt_truthy(xt_seq(doc, g_document_global))) {
483
483
  g_document_global = doc;
484
484
  xt_gc_add_root(&g_document_global);
485
485
  }
@@ -28,7 +28,8 @@ static xt_value make_dom_event(XtGuiWindow *win, const char *type, const xtgui::
28
28
  static const char *const fields[] = {
29
29
  "x", "y", "clientX", "clientY", "button", "clicks",
30
30
  "deltaX", "deltaY", "key", "code", "repeat", "ctrl",
31
- "shift", "alt", "meta",
31
+ "shift", "alt", "meta", "ctrlKey", "shiftKey", "altKey",
32
+ "metaKey",
32
33
  };
33
34
  for (const char *field : fields) {
34
35
  xt_value value = xt_object_get_cstr(legacy, field);
@@ -460,6 +460,10 @@ extern "C" xt_value xt_gui_run(int32_t argc, xt_value *argv) {
460
460
  SDL_DestroyGPUDevice(device);
461
461
  g_device = NULL;
462
462
  }
463
+ /* Audio streams belong to SDL, so close them before quitting the audio
464
+ * subsystem; otherwise a still-live AudioMixer's destructor runs after
465
+ * `SDL_Quit` and touches freed SDL state. */
466
+ xt_gui_audio_shutdown();
463
467
  if (g_sdl_initialized) {
464
468
  SDL_Quit();
465
469
  g_sdl_initialized = 0;
@@ -478,5 +482,8 @@ extern "C" xt_value xt_gui_quit(int32_t argc, xt_value *argv) {
478
482
  for (int i = 0; i < g_window_count; i++) {
479
483
  if (g_windows[i].open) xt_gui_quit_window(&g_windows[i]);
480
484
  }
485
+ /* A browser-style page never enters `xt_gui_run`, so stop its audio here
486
+ * too instead of leaving the device open until process exit. */
487
+ xt_gui_audio_shutdown();
481
488
  return XT_UNDEFINED;
482
489
  }
@@ -314,6 +314,15 @@ void xt_gui_dispatch_pointer(XtGuiWindow *win, const char *type, float x, float
314
314
  void xt_gui_dispatch_wheel(XtGuiWindow *win, float x, float y, float delta_x, float delta_y);
315
315
  /** Synthesise a key event (`keydown`/`keyup`). */
316
316
  void xt_gui_dispatch_key(XtGuiWindow *win, const char *type, const char *key, const char *code);
317
+ /** Translate one physical key (SDL scancode/keycode + modifiers) to the browser
318
+ * `key`/`code` names and dispatch it to the DOM and window listeners. */
319
+ void xt_gui_dispatch_physical_key(XtGuiWindow *win, const char *type, int scancode, int keycode,
320
+ bool repeat, bool ctrl, bool shift, bool alt, bool meta,
321
+ bool caps_lock);
322
+ /** Close every AudioContext's device and drop its graph. Called before
323
+ * `SDL_Quit` so an AudioMixer's destructor never touches an SDL stream after
324
+ * the audio subsystem was shut down. */
325
+ void xt_gui_audio_shutdown(void);
317
326
 
318
327
  /** The decoder for one media element, opening it (and its `src`) on first use.
319
328
  * Returns `nullptr` for a non-media element or an unreadable source. */
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  #include "gui_internal.h"
7
+ #include "gui_keymap.h"
7
8
  #include "listener_options.h"
8
9
 
9
10
  #include "paint.h"
@@ -216,21 +217,64 @@ static xt_value xt_gui_key_payload(const char *type, const char *key, const char
216
217
  xt_object_set(payload, xt_string_from_cstr("key"), xt_string_from_cstr(key));
217
218
  xt_object_set(payload, xt_string_from_cstr("code"), xt_string_from_cstr(code));
218
219
  xt_object_set(payload, xt_string_from_cstr("repeat"), repeat ? XT_TRUE : XT_FALSE);
220
+ /* Legacy DOM fields and their current `*Key` spellings: a page may read
221
+ * either, and WheelEvent/MouseEvent already expose both where they apply. */
219
222
  xt_object_set(payload, xt_string_from_cstr("ctrl"), ctrl ? XT_TRUE : XT_FALSE);
220
223
  xt_object_set(payload, xt_string_from_cstr("shift"), shift ? XT_TRUE : XT_FALSE);
221
224
  xt_object_set(payload, xt_string_from_cstr("alt"), alt ? XT_TRUE : XT_FALSE);
222
225
  xt_object_set(payload, xt_string_from_cstr("meta"), meta ? XT_TRUE : XT_FALSE);
226
+ xt_object_set(payload, xt_string_from_cstr("ctrlKey"), ctrl ? XT_TRUE : XT_FALSE);
227
+ xt_object_set(payload, xt_string_from_cstr("shiftKey"), shift ? XT_TRUE : XT_FALSE);
228
+ xt_object_set(payload, xt_string_from_cstr("altKey"), alt ? XT_TRUE : XT_FALSE);
229
+ xt_object_set(payload, xt_string_from_cstr("metaKey"), meta ? XT_TRUE : XT_FALSE);
223
230
  return payload;
224
231
  }
225
232
 
226
- void xt_gui_dispatch_key(XtGuiWindow *win, const char *type, const char *key, const char *code) {
233
+ static const xtgui::Node *xt_gui_find_body(const xtgui::Node *node) {
234
+ if (node == nullptr) return nullptr;
235
+ if (node->isElement() && node->tag == "body") return node;
236
+ for (const std::unique_ptr<xtgui::Node> &child : node->children) {
237
+ if (const xtgui::Node *found = xt_gui_find_body(child.get())) return found;
238
+ }
239
+ return nullptr;
240
+ }
241
+
242
+ /** The element a keyboard event targets when nothing is focused: `<body>`,
243
+ * like a browser, or the document root for a page without one. */
244
+ static const xtgui::Node *xt_gui_key_target(XtGuiWindow *win) {
245
+ if (win->document == nullptr) return nullptr;
246
+ if (const xtgui::Node *focused = win->document->focus()) return focused;
247
+ const xtgui::Node *root = win->document->root();
248
+ if (const xtgui::Node *body = xt_gui_find_body(root)) return body;
249
+ return root;
250
+ }
251
+
252
+ /** Deliver one fully built keyboard payload to the DOM (bubbling from the
253
+ * focused element / body to `document`) and then to `window` listeners. */
254
+ static void xt_gui_dispatch_key_payload(XtGuiWindow *win, const char *type, xt_value payload) {
227
255
  if (win == NULL || !win->open) return;
228
- xt_value payload = xt_gui_key_payload(type, key, code, false, false, false, false, false);
229
- const xtgui::Node *node = win->document != nullptr ? win->document->focus() : nullptr;
256
+ const xtgui::Node *node = xt_gui_key_target(win);
230
257
  if (node != nullptr) xt_gui_emit_dom_event(win, type, node, payload);
231
258
  xt_gui_emit_payload(win, type, payload);
232
259
  }
233
260
 
261
+ void xt_gui_dispatch_key(XtGuiWindow *win, const char *type, const char *key, const char *code) {
262
+ xt_value payload = xt_gui_key_payload(type, key, code, false, false, false, false, false);
263
+ xt_gui_dispatch_key_payload(win, type, payload);
264
+ }
265
+
266
+ void xt_gui_dispatch_physical_key(XtGuiWindow *win, const char *type, int scancode, int keycode,
267
+ bool repeat, bool ctrl, bool shift, bool alt, bool meta,
268
+ bool caps_lock) {
269
+ std::string key;
270
+ std::string code;
271
+ xt_gui_key_names(static_cast<SDL_Scancode>(scancode), static_cast<SDL_Keycode>(keycode), shift,
272
+ caps_lock, &key, &code);
273
+ xt_value payload = xt_gui_key_payload(type, key.c_str(), code.c_str(), repeat, ctrl, shift, alt,
274
+ meta);
275
+ xt_gui_dispatch_key_payload(win, type, payload);
276
+ }
277
+
234
278
  namespace guidev {
235
279
 
236
280
  void handle_event(const SDL_Event *event) {
@@ -306,13 +350,11 @@ void handle_event(const SDL_Event *event) {
306
350
  XtGuiWindow *win = xt_gui_find_by_window_id(event->key.windowID);
307
351
  if (win == NULL || !win->open) return;
308
352
  const char *type = event->type == SDL_EVENT_KEY_DOWN ? "keydown" : "keyup";
309
- xt_gui_emit_payload(win, type,
310
- xt_gui_key_payload(type, SDL_GetKeyName(event->key.key),
311
- SDL_GetScancodeName(event->key.scancode),
312
- event->key.repeat, (event->key.mod & SDL_KMOD_CTRL) != 0,
313
- (event->key.mod & SDL_KMOD_SHIFT) != 0,
314
- (event->key.mod & SDL_KMOD_ALT) != 0,
315
- (event->key.mod & SDL_KMOD_GUI) != 0));
353
+ xt_gui_dispatch_physical_key(
354
+ win, type, static_cast<int>(event->key.scancode), static_cast<int>(event->key.key),
355
+ event->key.repeat, (event->key.mod & SDL_KMOD_CTRL) != 0,
356
+ (event->key.mod & SDL_KMOD_SHIFT) != 0, (event->key.mod & SDL_KMOD_ALT) != 0,
357
+ (event->key.mod & SDL_KMOD_GUI) != 0, (event->key.mod & SDL_KMOD_CAPS) != 0);
316
358
  return;
317
359
  }
318
360
  }
@@ -0,0 +1,254 @@
1
+ /*
2
+ * xbintsc GUI engine — browser KeyboardEvent names from SDL key events.
3
+ *
4
+ * `code` is the *physical* key, which is what the DOM's `KeyboardEvent.code`
5
+ * reports and what games normally bind to. `key` is the produced value, so it
6
+ * follows Shift/Caps Lock for letters and Shift for the US punctuation row.
7
+ * The mapping is intentionally the US layout: SDL's key names are US-based,
8
+ * and a page that needs another layout should use `code` anyway.
9
+ */
10
+
11
+ #include "gui_keymap.h"
12
+
13
+ #include <cstdio>
14
+
15
+ namespace guidev {
16
+
17
+ namespace {
18
+
19
+ void set_names(std::string *key, std::string *code, const char *key_name, const char *code_name) {
20
+ *key = key_name;
21
+ *code = code_name;
22
+ }
23
+
24
+ /** Digits and their shifted symbols, indexed 0..9 for keys 1..0. */
25
+ const char *const kDigitKeys = "1234567890";
26
+ const char *const kDigitShifted = "!@#$%^&*()";
27
+
28
+ /** Keypad digits, indexed 0..9 for KP_1..KP_0. */
29
+ const char *const kKeypadDigits = "1234567890";
30
+
31
+ } // namespace
32
+
33
+ void xt_gui_key_names(SDL_Scancode scancode, SDL_Keycode keycode, bool shift, bool caps_lock,
34
+ std::string *key, std::string *code) {
35
+ key->clear();
36
+ code->clear();
37
+
38
+ /* Letters: SDL numbers A..Z consecutively. Caps Lock and Shift cancel out. */
39
+ if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
40
+ char letter = static_cast<char>('A' + (scancode - SDL_SCANCODE_A));
41
+ *code = "Key";
42
+ code->push_back(letter);
43
+ bool upper = shift != caps_lock;
44
+ key->push_back(upper ? letter : static_cast<char>(letter + ('a' - 'A')));
45
+ return;
46
+ }
47
+
48
+ /* Number row. */
49
+ if (scancode >= SDL_SCANCODE_1 && scancode <= SDL_SCANCODE_0) {
50
+ int index = static_cast<int>(scancode - SDL_SCANCODE_1);
51
+ *code = "Digit";
52
+ code->push_back(kDigitKeys[index]);
53
+ key->push_back(shift ? kDigitShifted[index] : kDigitKeys[index]);
54
+ return;
55
+ }
56
+
57
+ /* Function keys: F1..F12 and F13..F24 are two contiguous runs; the gap
58
+ * between F12 and F13 contains PrintScreen, arrows and the keypad. */
59
+ if ((scancode >= SDL_SCANCODE_F1 && scancode <= SDL_SCANCODE_F12) ||
60
+ (scancode >= SDL_SCANCODE_F13 && scancode <= SDL_SCANCODE_F24)) {
61
+ int number;
62
+ if (scancode <= SDL_SCANCODE_F12) {
63
+ number = static_cast<int>(scancode - SDL_SCANCODE_F1) + 1;
64
+ } else {
65
+ number = static_cast<int>(scancode - SDL_SCANCODE_F13) + 13;
66
+ }
67
+ char name[8];
68
+ std::snprintf(name, sizeof(name), "F%d", number);
69
+ *key = name;
70
+ *code = name;
71
+ return;
72
+ }
73
+
74
+ /* Keypad digits, same 1..9,0 order. */
75
+ if (scancode >= SDL_SCANCODE_KP_1 && scancode <= SDL_SCANCODE_KP_0) {
76
+ int index = static_cast<int>(scancode - SDL_SCANCODE_KP_1);
77
+ *code = "Numpad";
78
+ code->push_back(kKeypadDigits[index]);
79
+ key->push_back(kKeypadDigits[index]);
80
+ return;
81
+ }
82
+
83
+ switch (scancode) {
84
+ case SDL_SCANCODE_RETURN:
85
+ case SDL_SCANCODE_RETURN2:
86
+ set_names(key, code, "Enter", "Enter");
87
+ return;
88
+ case SDL_SCANCODE_ESCAPE:
89
+ set_names(key, code, "Escape", "Escape");
90
+ return;
91
+ case SDL_SCANCODE_BACKSPACE:
92
+ set_names(key, code, "Backspace", "Backspace");
93
+ return;
94
+ case SDL_SCANCODE_TAB:
95
+ set_names(key, code, "Tab", "Tab");
96
+ return;
97
+ case SDL_SCANCODE_SPACE:
98
+ set_names(key, code, " ", "Space");
99
+ return;
100
+ case SDL_SCANCODE_CAPSLOCK:
101
+ set_names(key, code, "CapsLock", "CapsLock");
102
+ return;
103
+ case SDL_SCANCODE_PRINTSCREEN:
104
+ set_names(key, code, "PrintScreen", "PrintScreen");
105
+ return;
106
+ case SDL_SCANCODE_SCROLLLOCK:
107
+ set_names(key, code, "ScrollLock", "ScrollLock");
108
+ return;
109
+ case SDL_SCANCODE_PAUSE:
110
+ set_names(key, code, "Pause", "Pause");
111
+ return;
112
+ case SDL_SCANCODE_INSERT:
113
+ set_names(key, code, "Insert", "Insert");
114
+ return;
115
+ case SDL_SCANCODE_HOME:
116
+ set_names(key, code, "Home", "Home");
117
+ return;
118
+ case SDL_SCANCODE_PAGEUP:
119
+ set_names(key, code, "PageUp", "PageUp");
120
+ return;
121
+ case SDL_SCANCODE_DELETE:
122
+ set_names(key, code, "Delete", "Delete");
123
+ return;
124
+ case SDL_SCANCODE_END:
125
+ set_names(key, code, "End", "End");
126
+ return;
127
+ case SDL_SCANCODE_PAGEDOWN:
128
+ set_names(key, code, "PageDown", "PageDown");
129
+ return;
130
+ case SDL_SCANCODE_RIGHT:
131
+ set_names(key, code, "ArrowRight", "ArrowRight");
132
+ return;
133
+ case SDL_SCANCODE_LEFT:
134
+ set_names(key, code, "ArrowLeft", "ArrowLeft");
135
+ return;
136
+ case SDL_SCANCODE_DOWN:
137
+ set_names(key, code, "ArrowDown", "ArrowDown");
138
+ return;
139
+ case SDL_SCANCODE_UP:
140
+ set_names(key, code, "ArrowUp", "ArrowUp");
141
+ return;
142
+ case SDL_SCANCODE_NUMLOCKCLEAR:
143
+ set_names(key, code, "NumLock", "NumLock");
144
+ return;
145
+ case SDL_SCANCODE_KP_ENTER:
146
+ set_names(key, code, "Enter", "NumpadEnter");
147
+ return;
148
+ case SDL_SCANCODE_KP_DIVIDE:
149
+ set_names(key, code, "/", "NumpadDivide");
150
+ return;
151
+ case SDL_SCANCODE_KP_MULTIPLY:
152
+ set_names(key, code, "*", "NumpadMultiply");
153
+ return;
154
+ case SDL_SCANCODE_KP_MINUS:
155
+ set_names(key, code, "-", "NumpadSubtract");
156
+ return;
157
+ case SDL_SCANCODE_KP_PLUS:
158
+ set_names(key, code, "+", "NumpadAdd");
159
+ return;
160
+ case SDL_SCANCODE_KP_PERIOD:
161
+ case SDL_SCANCODE_KP_DECIMAL:
162
+ set_names(key, code, ".", "NumpadDecimal");
163
+ return;
164
+ case SDL_SCANCODE_KP_EQUALS:
165
+ set_names(key, code, "=", "NumpadEqual");
166
+ return;
167
+ case SDL_SCANCODE_KP_COMMA:
168
+ set_names(key, code, ",", "NumpadComma");
169
+ return;
170
+ case SDL_SCANCODE_APPLICATION:
171
+ case SDL_SCANCODE_MENU:
172
+ set_names(key, code, "ContextMenu", "ContextMenu");
173
+ return;
174
+ case SDL_SCANCODE_LCTRL:
175
+ set_names(key, code, "Control", "ControlLeft");
176
+ return;
177
+ case SDL_SCANCODE_RCTRL:
178
+ set_names(key, code, "Control", "ControlRight");
179
+ return;
180
+ case SDL_SCANCODE_LSHIFT:
181
+ set_names(key, code, "Shift", "ShiftLeft");
182
+ return;
183
+ case SDL_SCANCODE_RSHIFT:
184
+ set_names(key, code, "Shift", "ShiftRight");
185
+ return;
186
+ case SDL_SCANCODE_LALT:
187
+ set_names(key, code, "Alt", "AltLeft");
188
+ return;
189
+ case SDL_SCANCODE_RALT:
190
+ set_names(key, code, "Alt", "AltRight");
191
+ return;
192
+ case SDL_SCANCODE_LGUI:
193
+ set_names(key, code, "Meta", "MetaLeft");
194
+ return;
195
+ case SDL_SCANCODE_RGUI:
196
+ set_names(key, code, "Meta", "MetaRight");
197
+ return;
198
+ default:
199
+ break;
200
+ }
201
+
202
+ /* Remaining US punctuation, keyed by scancode. */
203
+ switch (scancode) {
204
+ case SDL_SCANCODE_MINUS:
205
+ set_names(key, code, shift ? "_" : "-", "Minus");
206
+ return;
207
+ case SDL_SCANCODE_EQUALS:
208
+ set_names(key, code, shift ? "+" : "=", "Equal");
209
+ return;
210
+ case SDL_SCANCODE_LEFTBRACKET:
211
+ set_names(key, code, shift ? "{" : "[", "BracketLeft");
212
+ return;
213
+ case SDL_SCANCODE_RIGHTBRACKET:
214
+ set_names(key, code, shift ? "}" : "]", "BracketRight");
215
+ return;
216
+ case SDL_SCANCODE_BACKSLASH:
217
+ case SDL_SCANCODE_NONUSHASH:
218
+ set_names(key, code, shift ? "|" : "\\", "Backslash");
219
+ return;
220
+ case SDL_SCANCODE_SEMICOLON:
221
+ set_names(key, code, shift ? ":" : ";", "Semicolon");
222
+ return;
223
+ case SDL_SCANCODE_APOSTROPHE:
224
+ set_names(key, code, shift ? "\"" : "'", "Quote");
225
+ return;
226
+ case SDL_SCANCODE_GRAVE:
227
+ set_names(key, code, shift ? "~" : "`", "Backquote");
228
+ return;
229
+ case SDL_SCANCODE_COMMA:
230
+ set_names(key, code, shift ? "<" : ",", "Comma");
231
+ return;
232
+ case SDL_SCANCODE_PERIOD:
233
+ set_names(key, code, shift ? ">" : ".", "Period");
234
+ return;
235
+ case SDL_SCANCODE_SLASH:
236
+ set_names(key, code, shift ? "?" : "/", "Slash");
237
+ return;
238
+ case SDL_SCANCODE_NONUSBACKSLASH:
239
+ set_names(key, code, "\\", "IntlBackslash");
240
+ return;
241
+ default:
242
+ break;
243
+ }
244
+
245
+ /* Unknown key: keep SDL's human-readable names. `key` comes from the
246
+ * keycode so a consumer matching "Up" still sees it on a layout whose
247
+ * scancode name differs. */
248
+ const char *key_name = SDL_GetKeyName(keycode);
249
+ const char *code_name = SDL_GetScancodeName(scancode);
250
+ set_names(key, code, key_name != nullptr ? key_name : "",
251
+ code_name != nullptr ? code_name : "");
252
+ }
253
+
254
+ } // namespace guidev
@@ -0,0 +1,31 @@
1
+ /*
2
+ * xbintsc GUI engine — SDL key event to browser KeyboardEvent names.
3
+ *
4
+ * The DOM exposes the browser spellings `KeyboardEvent.key` (the produced
5
+ * character / action name) and `code` (the physical key), while SDL reports a
6
+ * scancode and keycode. A page written for a browser matches on `code`
7
+ * ("KeyW", "ArrowUp", …), so translating `SDL_GetScancodeName` directly
8
+ * ("W", "Up") is not enough.
9
+ */
10
+ #ifndef XT_GUI_KEYMAP_H
11
+ #define XT_GUI_KEYMAP_H
12
+
13
+ #include <SDL3/SDL.h>
14
+
15
+ #include <string>
16
+
17
+ namespace guidev {
18
+
19
+ /** Fill `key` and `code` with the browser names for one physical key press.
20
+ *
21
+ * `scancode` selects the physical `code`; `keycode` and the modifier state
22
+ * select the produced `key` (letters honour Shift and Caps Lock, digits and
23
+ * punctuation use the US layout, which matches SDL's default key names).
24
+ * Unrecognised keys fall back to SDL's own names rather than an empty string,
25
+ * so a page that only logs events still sees something useful. */
26
+ void xt_gui_key_names(SDL_Scancode scancode, SDL_Keycode keycode, bool shift, bool caps_lock,
27
+ std::string *key, std::string *code);
28
+
29
+ } // namespace guidev
30
+
31
+ #endif /* XT_GUI_KEYMAP_H */
@@ -380,7 +380,22 @@ std::string DisplayList::dump() const {
380
380
  } else {
381
381
  const PaintImage &image = images[op.index];
382
382
  suffix = transformSuffix(image.transform);
383
- if (image.media_id != 0) {
383
+ if (image.gradient != nullptr) {
384
+ const PaintGradient &gradient = *image.gradient;
385
+ std::snprintf(buffer, sizeof(buffer),
386
+ "image x=%.1f y=%.1f w=%.1f h=%.1f gradient=%s center=%.1f,%.1f "
387
+ "radius=%.1f,%.1f stops=",
388
+ image.x, image.y, image.width, image.height,
389
+ gradient.repeating ? "repeating-radial" : "radial", gradient.center_x,
390
+ gradient.center_y, gradient.radius_x, gradient.radius_y);
391
+ text += prefix + buffer;
392
+ for (size_t stop = 0; stop < gradient.stops.size(); stop++) {
393
+ std::snprintf(buffer, sizeof(buffer), "%s%s@%.4f", stop == 0 ? "" : ",",
394
+ formatColor(gradient.stops[stop].color).c_str(),
395
+ gradient.stops[stop].offset);
396
+ text += buffer;
397
+ }
398
+ } else if (image.media_id != 0) {
384
399
  std::snprintf(buffer, sizeof(buffer), "image x=%.1f y=%.1f w=%.1f h=%.1f media=%llu",
385
400
  image.x, image.y, image.width, image.height,
386
401
  (unsigned long long)image.media_id);