buninu-browser 0.4.0 → 0.5.1

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 (99) hide show
  1. package/README.md +22 -1
  2. package/frontends/wayland/README.md +50 -21
  3. package/frontends/wayland/index.js +14 -0
  4. package/frontends/wayland/input.js +24 -5
  5. package/frontends/wayland/urlbar.js +9 -5
  6. package/lib/audio/shared-connection.js +31 -0
  7. package/lib/cdp-server.js +15 -2
  8. package/lib/happy-dom/inert-link.js +26 -0
  9. package/lib/happy-dom/node-iterator-removal.js +36 -0
  10. package/lib/happy-dom/node-replacement.js +60 -0
  11. package/lib/happy-dom/parser.js +4 -1
  12. package/lib/headless-shell.js +7 -4
  13. package/lib/layout/text-layout.js +4 -2
  14. package/lib/paint/display-list.js +2 -1
  15. package/lib/qml/README.md +456 -0
  16. package/lib/qml/builtins.js +76 -5
  17. package/lib/qml/canvas.js +76 -0
  18. package/lib/qml/document.js +47 -24
  19. package/lib/qml/fonts.js +69 -0
  20. package/lib/qml/host.js +28 -0
  21. package/lib/qml/loader.js +7 -3
  22. package/lib/qml/models.js +1 -1
  23. package/lib/qml/reactive.js +26 -3
  24. package/lib/qml/runtime.js +263 -41
  25. package/lib/qml/shader/fragment.js +83 -0
  26. package/lib/qml/shader/qsb.js +27 -0
  27. package/lib/qml/shader/renderer.js +39 -0
  28. package/lib/qml/shader/runtime.js +61 -0
  29. package/lib/qml/shapes.js +9 -1
  30. package/lib/qml/shortcuts.js +65 -0
  31. package/lib/qml/textures.js +4 -1
  32. package/lib/qml/window-constraints.js +16 -0
  33. package/lib/renderer/bindings.js +110 -4
  34. package/lib/renderer/canvas-2d.js +16 -0
  35. package/lib/renderer/document-selection.js +19 -0
  36. package/lib/renderer/host.js +14 -11
  37. package/lib/renderer/main.js +2 -0
  38. package/lib/renderer/page-realm.js +4 -9
  39. package/lib/renderer/page-renderer.js +283 -39
  40. package/lib/renderer/pointer-move.js +5 -3
  41. package/lib/renderer/render-interval.js +9 -0
  42. package/lib/style/computed-style.js +8 -2
  43. package/package.json +1 -1
  44. package/test/audio-connection-lifetime.test.js +46 -0
  45. package/test/canvas-paint.test.js +8 -0
  46. package/test/compare-node-iterator.js +42 -0
  47. package/test/computed-style.test.js +12 -0
  48. package/test/document-selection.test.js +27 -0
  49. package/test/double-click.test.js +85 -0
  50. package/test/fixtures/canvas-damage.js +40 -0
  51. package/test/fixtures/fake-renderer.js +1 -0
  52. package/test/fixtures/fractional-text-scale.js +38 -0
  53. package/test/fixtures/node-iterator-cases.js +59 -0
  54. package/test/fixtures/node-iterator-chromium.json +315 -0
  55. package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
  56. package/test/fixtures/omastorm-helpers/check.mjs +56 -0
  57. package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
  58. package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
  59. package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
  60. package/test/fixtures/pointer-paint-batching.js +23 -0
  61. package/test/fixtures/qml-fonts/main.qml +10 -0
  62. package/test/fixtures/qml-locale/check.mjs +6 -0
  63. package/test/fixtures/qml-locale/main.qml +8 -0
  64. package/test/fixtures/qml-shader/grab.qml +14 -0
  65. package/test/fixtures/qml-shader/gradient.frag +5 -0
  66. package/test/fixtures/qml-shader/main.qml +14 -0
  67. package/test/fixtures/qml-shader/texture-grab.qml +25 -0
  68. package/test/fixtures/qml-shader/texture-size.frag +5 -0
  69. package/test/fixtures/qml-shader-check.mjs +24 -0
  70. package/test/fixtures/title-no-paint.js +20 -0
  71. package/test/fractional-text-scale.test.js +6 -0
  72. package/test/inert-link.test.js +37 -0
  73. package/test/manual-double-click.html +21 -0
  74. package/test/manual-mouse-buttons.html +18 -0
  75. package/test/manual-omastorm-engine.mjs +19 -0
  76. package/test/manual-omastorm-shaders.mjs +28 -0
  77. package/test/manual-omastorm-software.mjs +25 -0
  78. package/test/manual-pointer-scroll.html +43 -0
  79. package/test/node-iterator.test.js +81 -0
  80. package/test/page-scripts.test.js +116 -1
  81. package/test/pointer-drag.test.js +104 -0
  82. package/test/qml-canvas.test.js +21 -0
  83. package/test/qml-connections-timing.test.js +22 -0
  84. package/test/qml-controls-window.test.js +182 -0
  85. package/test/qml-fonts.test.js +31 -0
  86. package/test/qml-grid-multiline.test.js +48 -0
  87. package/test/qml-locale.test.js +45 -0
  88. package/test/qml-nonvisual-visible.test.js +19 -0
  89. package/test/qml-shader-runtime.test.js +6 -0
  90. package/test/qml-shortcuts.test.js +39 -0
  91. package/test/qml-socket.test.js +60 -0
  92. package/test/qml-software-shader.test.js +55 -0
  93. package/test/qml-window-update.test.js +36 -0
  94. package/test/render-interval.test.js +33 -0
  95. package/test/text-layout.test.js +15 -0
  96. package/test/title-no-paint.test.js +6 -0
  97. package/test/wayland-scroll.test.js +40 -0
  98. package/test/wayland-urlbar.test.js +23 -0
  99. package/test/window-resize.test.js +29 -0
package/lib/qml/README.md CHANGED
@@ -969,3 +969,459 @@ remain in Flea's thumbnail cache until invalidated or removed.
969
969
  媒體資訊。官方 0.3.12 後端已實測接受並寫入縮圖快取,沿用實驗容器的
970
970
  bwrap 繞過路徑,無須修改 Flea QML。刪除註冊檔可停用,但既有佔位縮圖
971
971
  仍保留在快取,直到失效或被移除。
972
+
973
+ ## Omastorm container
974
+
975
+ ### Android / Termux setup
976
+
977
+ Termux's terminal does not support Kitty graphics. Start Land inside jsgotty's
978
+ browser terminal instead; keep container build commands in a separate native
979
+ Termux shell. These steps are self-contained and do not require the Flea setup.
980
+
981
+ 1. **Install Bun and start jsgotty in native Termux**:
982
+
983
+ ```sh
984
+ npm i -g bun
985
+ bun --version
986
+ bunx jsgotty -w -r --session bash
987
+ ```
988
+
989
+ Open the complete URL printed by jsgotty in your phone's browser or WebView.
990
+ `-w` permits terminal input, `-r` generates that random URL, and `--session`
991
+ keeps the shell available for reconnection. This server-side `-r` is different
992
+ from the CLI client's `--client -r PID`.
993
+
994
+ For on-screen Super, Alt and Ctrl keys, you can first build a minapk app:
995
+
996
+ ```sh
997
+ npx @drxiaozhi/minapk
998
+ ```
999
+
1000
+ Install and open the generated APK. Inside the app, press **Volume Up**,
1001
+ choose **Go to URL**, and enter the complete jsgotty URL. Use the app's
1002
+ modifier keys for shortcuts; **long-press Alt to activate Super**.
1003
+
1004
+ 2. **Start Land inside that browser terminal** and leave it running:
1005
+
1006
+ ```sh
1007
+ env XDG_RUNTIME_DIR="$TMPDIR" bunx buninu-land
1008
+ ```
1009
+
1010
+ The first launch may download Land and Casty. Its display stays in the browser
1011
+ terminal; the default socket is `$TMPDIR/wayland-buninu`. Use that default
1012
+ socket for the commands below. An existing Land instance can cause a numbered
1013
+ name such as `wayland-buninu1`; check the address shown by Land.
1014
+
1015
+ 3. **Open another native Termux shell and install js-udocker**. Do not run these
1016
+ commands in Land's address bar or inside a Debian proot. Skip package
1017
+ installation if these tools are already installed:
1018
+
1019
+ ```sh
1020
+ pkg install git nodejs proot curl tar
1021
+ cd "$HOME"
1022
+ git clone https://github.com/jjtseng93/js-udocker.git
1023
+ cd js-udocker
1024
+ sh termux_install.sh
1025
+ hash -r
1026
+ command -v udocker
1027
+ ```
1028
+
1029
+ The installer points `udocker` at this checkout and uses Node.js and proot.
1030
+ Keep the checkout in place. If js-udocker is already installed, reuse it.
1031
+
1032
+ 4. **Get buninu-browser as the build context**, still in the second shell.
1033
+ Reuse your checkout if it already contains this Dockerfile:
1034
+
1035
+ ```sh
1036
+ cd "$HOME"
1037
+ git clone https://github.com/jjtseng93/buninu-browser.git
1038
+ cd buninu-browser
1039
+ test -f test/fixtures/omastorm-helpers/Dockerfile
1040
+ test -S "$TMPDIR/wayland-buninu"
1041
+ ```
1042
+
1043
+ No `bun install` is needed in the checkout. A socket file alone does not prove
1044
+ Land is still running: `ECONNREFUSED` means you must restart Land.
1045
+
1046
+ 5. **Build and run from the repository root**:
1047
+
1048
+ ```sh
1049
+ udocker build -t omastorm-qml -y -f test/fixtures/omastorm-helpers/Dockerfile .
1050
+ udocker run -b "$TMPDIR:/tmp" omastorm-qml
1051
+ ```
1052
+
1053
+ Return to the browser terminal to use Omastorm. The bind exposes the host's
1054
+ Land socket at `/tmp/wayland-buninu`; no extra startup arguments or font
1055
+ environment variables are needed. Do not start another Land inside the
1056
+ container. If deliberately using a numbered socket, pass
1057
+ `-e WAYLAND_DISPLAY=/tmp/wayland-buninu1` before `omastorm-qml` in the run
1058
+ command instead.
1059
+
1060
+ ### Linux Docker
1061
+
1062
+ Start `bunx buninu-land` on the host in a terminal supporting Kitty graphics.
1063
+ Then build from the buninu-browser repository root in another shell:
1064
+
1065
+ ```sh
1066
+ docker build -t omastorm-qml -f test/fixtures/omastorm-helpers/Dockerfile .
1067
+ docker run --rm -v "${XDG_RUNTIME_DIR:-/tmp}:/tmp" omastorm-qml
1068
+ ```
1069
+
1070
+ The mounted directory must contain the running Land socket named
1071
+ `wayland-buninu`, and the container user must be able to connect to it.
1072
+ Docker's `--rm` removes container settings after exit; arrange persistent
1073
+ storage separately if needed.
1074
+
1075
+ ### Image contents and validation
1076
+
1077
+ The following explains the image and its checks; no additional manual
1078
+ installation is needed after the steps above.
1079
+
1080
+ - [Dockerfile](../../test/fixtures/omastorm-helpers/Dockerfile) uses `oven/bun:1.4.4`,
1081
+ single-stage COPY/RUN, one `ENV KEY=value` per line and JSON CMD, matching the
1082
+ Flea recipe and js-udocker parser.
1083
+ - It downloads Omastorm revision `492e97841146bd9d6d9f7a3064e5e1ae9840cc68`
1084
+ (manifest 0.1.23) and the official engine 0.1.15 via upstream's pinned release
1085
+ installer and checksums, for aarch64 or x86_64. Rust compilation is not needed.
1086
+ - It downloads JetBrainsMono Nerd Font Mono Regular from Nerd Fonts v3.4.0,
1087
+ including its OFL license, and sets `QT_QPA_FONTDIR` inside the image.
1088
+ - Browser code and its existing WASM/fonts are copied from the build context;
1089
+ no npm dependencies are installed into your checkout. Rebuild after changing
1090
+ browser code. Native Qt, Quickshell, Mesa and GPU device access are unnecessary.
1091
+ - [prepare.mjs](../../test/fixtures/omastorm-helpers/prepare.mjs) removes only the
1092
+ two explicit Software-backend refusal checks in this container's Omastorm UI.
1093
+ It saves `.upstream` originals and refuses unexpected source changes. Actual
1094
+ shader compilation and texture verification remain enabled; this is the same
1095
+ experimental software path as the manual Omastorm demonstration.
1096
+ - [launch.mjs](../../test/fixtures/omastorm-helpers/launch.mjs) starts an owned
1097
+ engine in a unique temporary runtime directory, then opens the QML frontend.
1098
+ It keeps the Land socket at its original absolute path, so sharing `/tmp`
1099
+ does not reuse or stop another Omastorm engine. Closing the window or stopping
1100
+ the launcher closes its engine and removes its temporary textures/socket.
1101
+ - [check.mjs](../../test/fixtures/omastorm-helpers/check.mjs) runs during build:
1102
+ verifies Bun version, the official engine executable, all four software
1103
+ shaders, Nerd Font glyph coverage and a QML screenshot. It needs no Land or
1104
+ weather downloads. Require the final `PASS:` message; js-udocker versions
1105
+ which continue after failed RUN steps may still report overall success.
1106
+ - After binding `/tmp`, verify the real engine Unix-socket hello/site list with
1107
+ `udocker run -b "$TMPDIR:/tmp" omastorm-qml bun /usr/local/lib/omastorm-helpers/check.mjs --engine`.
1108
+ This is separate because js-udocker's unbound build ROOT/tmp path can exceed
1109
+ the Unix socket path-length limit. The normal launcher also waits for its
1110
+ engine socket before opening QML.
1111
+ - Live radar/map data still needs network access and a selected station. A fresh
1112
+ profile may ask for a location; use the application's picker. Build checks do
1113
+ not promise available live weather, full native-QML parity or GPU performance.
1114
+
1115
+ ## Omastorm compatibility experiments
1116
+
1117
+ The first compatibility stage provides local `Quickshell.Io.Socket` with Unix
1118
+ socket connections, UTF-8 `SplitParser` messages, writes, disconnect/reconnect,
1119
+ and document-owned cleanup. `connected` reads the actual connection state;
1120
+ assigning it requests a connection/disconnection. Writes are queued immediately;
1121
+ `flush()` is accepted without adding a second buffer. This does not expose host
1122
+ sockets to ordinary HTML or remote QML.
1123
+
1124
+ QML `Canvas` reuses the browser's HTML canvas and 2D context. The adapter supplies
1125
+ paint scheduling, dimensions, QML color conversion, reset, and cancellation when
1126
+ disposed. It currently supports `requestPaint`, `markDirty` (full repaint),
1127
+ `contextType: "2d"`, and animation callbacks. QML image-loading extensions,
1128
+ canvas tiling and alternate render strategies remain unsupported. Software
1129
+ `ShaderEffect` and image-loading support are described in the later experiment
1130
+ sections. `QtQuick.Window` imports resolve existing Window attached behavior;
1131
+ this does not implement every type from that module.
1132
+
1133
+ To probe an existing official Omastorm engine using the unmodified `Engine.qml`:
1134
+
1135
+ ```sh
1136
+ env XDG_RUNTIME_DIR=/tmp bun test/manual-omastorm-engine.mjs /path/to/omastorm
1137
+ ```
1138
+
1139
+ The probe does not download or start the engine. This stage has received the
1140
+ real engine's site list and polar-frame state, and rendered `RadarMark.qml`.
1141
+ That initial milestone has since progressed to the experimental software radar
1142
+ window described above; it does not imply full native Quickshell compatibility.
1143
+
1144
+ ### Omastorm 相容性實驗
1145
+
1146
+ 第一階段加入本地 `Quickshell.Io.Socket`:Unix socket 連線、UTF-8
1147
+ `SplitParser` 訊息、寫入、斷線重連,以及頁面關閉時清理。`connected` 讀取實際
1148
+ 連線狀態,賦值則要求連線/斷線;寫入立即排入傳輸,`flush()` 不另建緩衝。
1149
+ 一般 HTML 與遠端 QML 不會因此取得 host socket 能力。
1150
+
1151
+ QML `Canvas` 重用瀏覽器的 HTML canvas/2D context,只轉接重繪排程、尺寸、
1152
+ QML 顏色、reset 與銷毀清理。目前支援 `requestPaint`、`markDirty`(整張重繪)、
1153
+ `contextType: "2d"` 與動畫回呼;QML 圖片載入擴充、畫布分塊、其他渲染策略與
1154
+ 自訂 `ShaderEffect` 已有實驗性軟體子集,詳見後段說明。`QtQuick.Window` import 可使用既有 Window
1155
+ 附加行為,不代表該模組所有類型皆已實作。
1156
+
1157
+ 上方命令可使用未修改的 `Engine.qml` 連到已啟動的官方後端,不會下載或啟動後端。
1158
+ 已實測收到站點清單與 polar frame 狀態,並渲染原版 `RadarMark.qml`;完整 shell
1159
+ 已由此初期成果推進到後段的軟體雷達實驗,詳見下方 ShaderEffect 與 Omastorm 軟體版應用說明;仍不代表完整相容。
1160
+
1161
+ ### Keyboard shortcuts and object delegates
1162
+
1163
+ `Shortcut` supports string `sequence`/`sequences`, portable key names,
1164
+ Ctrl/Alt/Shift/Meta modifiers, up to four key strokes (one-second prefix timeout),
1165
+ enabled/autoRepeat, and activated/activatedAmbiguously signals. It consumes
1166
+ matching DOM keydown events before ordinary key handlers. Window scope uses the
1167
+ existing QML window/focus state; ApplicationShortcut applies across the document's
1168
+ windows. Native text currently uses portable text. Numeric StandardKey bindings,
1169
+ platform-localized key names and Keys.shortcutOverride are not implemented.
1170
+
1171
+ `Instantiator` reuses Repeater's scoped factories for synchronous array/count
1172
+ models. Objects are parented to the Instantiator; active, count, object/objectAt,
1173
+ objectAdded/objectRemoved, model replacement and disposal are supported. Dynamic
1174
+ delegate replacement and richer model adapters remain unsupported; asynchronous
1175
+ creation is rejected explicitly. As with Repeater, a model replacement currently
1176
+ recreates all delegates rather than preserving identities by row diff.
1177
+
1178
+ ### 快捷鍵與物件委派
1179
+
1180
+ `Shortcut` 支援字串 sequence/sequences、可攜式鍵名、Ctrl/Alt/Shift/Meta、
1181
+ 最多四段按鍵(前綴等待一秒)、enabled/autoRepeat,以及 activated/
1182
+ activatedAmbiguously。透過 DOM keydown 攔截匹配按鍵,視窗範圍沿用既有 QML 焦點
1183
+ 狀態。nativeText 暫用 portableText;數字 StandardKey、平台翻譯鍵名與
1184
+ Keys.shortcutOverride 尚未支援。
1185
+
1186
+ `Instantiator` 重用 Repeater 的作用域工廠,同步建立陣列/數量模型的物件。
1187
+ 支援正確父物件、active、count、object/objectAt、增加/移除訊號、模型替換與
1188
+ 銷毀清理。動態 delegate 替換及其他模型轉接尚未支援,非同步建立會明確報錯。
1189
+ 目前模型替換會重建所有委派,尚未以逐列差異保留物件身分。
1190
+
1191
+ `PathMultiline` now emits independent SVG subpaths; the existing canvas ShapePath
1192
+ fallback also accepts the same geometry. `GridLayout` supports sequential
1193
+ row-major placement with columns/rows, spacing, preferred/minimum sizes,
1194
+ fill/alignment and implicit dimensions. Explicit cell coordinates, spans,
1195
+ right-to-left flow and uniform-cell options are not implemented. Nested layouts
1196
+ retain intrinsic preferred dimensions instead of feeding allocated widths back
1197
+ into the parent. The original Omastorm KeysSheet has been screenshot-checked with
1198
+ both columns visible; this does not validate the shader-dependent radar window.
1199
+
1200
+ `PathMultiline` 已接到 SVG 子路徑,既有 canvas ShapePath 路徑也接受同一份幾何。
1201
+ `GridLayout` 支援逐列排列、欄列數、間距、偏好/最小尺寸、填滿/對齊與自然尺寸;
1202
+ 指定格位、跨格、由右至左及等寬格選項尚未支援。巢狀 Layout 不再把父層分配的
1203
+ 寬度反灌成自然寬度。已截圖確認原版 KeysSheet 雙欄正常,但不代表雷達視窗已可用。
1204
+
1205
+ ### Experimental software ShaderEffect
1206
+
1207
+ `shader/fragment.js` experimentally translates a Qt-style GLSL fragment subset
1208
+ to SkSL and compiles it with the existing CanvasKit RuntimeEffect. CPU raster
1209
+ surfaces execute it without EGL, Vulkan, WebGL, new native libraries or a second
1210
+ production CanvasKit instance. The caller supplies existing child shaders and
1211
+ uniforms; the drawing path does not encode images or read pixels back into JS.
1212
+ Only tests read pixels to verify results.
1213
+
1214
+ ```sh
1215
+ bun test test/qml-software-shader.test.js
1216
+ bun test/manual-omastorm-shaders.mjs /path/to/omastorm
1217
+ ```
1218
+
1219
+ The offline manual probe compiles all four upstream `.frag.qsb` packages and checks
1220
+ known pixels for tile and texture-check. Radar/grid compilation is **not** a
1221
+ visual correctness or performance result. The current translator supports one
1222
+ vec4 output, qt_TexCoord0, simple uniform blocks, scalar/vector/matrix uniforms,
1223
+ 2D child sampling and level-zero textureSize. It is not a complete GLSL compiler;
1224
+ preprocessing, custom vertex shaders and additional interfaces are unsupported.
1225
+ Native shader compilation errors remain errors, never successful placeholders.
1226
+
1227
+ Local QML ShaderEffect now accepts raw GLSL or QSB v9 desktop GLSL records.
1228
+ Numeric/vector/color uniforms update reactively; Image and ShaderEffectSource
1229
+ samplers reuse renderer images, and output goes directly into an HTML canvas.
1230
+ Resources are disposed on item destruction or page teardown. No PNG encoding,
1231
+ base64 or JS pixel readback is introduced in the rendering path.
1232
+
1233
+ Run the self-contained clickable gradient with:
1234
+
1235
+ ```sh
1236
+ bun lib/qml/qswayland -p test/fixtures/qml-shader/main.qml
1237
+ ```
1238
+
1239
+ An original Omastorm tile QSB and cached map texture have rendered on CPU.
1240
+ A patched application copy now displays a polar RadarMap; large mosaic
1241
+ TextureCheck and complete UI compatibility are not yet verified. Custom
1242
+ vertex shaders, mesh/culling changes, full GLSL and other QSB versions remain
1243
+ unsupported. GPU acceleration remains optional future work.
1244
+
1245
+ ### 實驗性純軟體 ShaderEffect
1246
+
1247
+ `shader/fragment.js` 實驗性地把 Qt 風格 GLSL fragment 子集轉成 SkSL,重用現有
1248
+ CanvasKit RuntimeEffect,在 CPU raster surface 執行,不需要 EGL、Vulkan、
1249
+ WebGL 或新的原生依賴。材質由同一個 CanvasKit 的 child shader 提供,繪製本身
1250
+ 不做圖片編碼或像素讀回;測試才讀回像素驗證。
1251
+
1252
+ 上方離線實驗已編譯原版四份 `.frag.qsb`,並驗證 tile/texture-check 的已知像素。
1253
+ radar 已在完整介面顯示極座標回波;grid 仍只有編譯驗證,尚未確認大型拼接圖。轉譯器僅支援
1254
+ 單一 vec4 輸出、qt_TexCoord0、簡單 uniform block、數值/向量/矩陣 uniform、
1255
+ 二維材質取樣及第零層 textureSize,不是完整 GLSL 編譯器。
1256
+
1257
+ 本地 QML 已接上 QSB v9 的桌面 GLSL、數值/向量/色彩 uniform、Image 與
1258
+ ShaderEffectSource 材質,以及重繪與資源釋放。輸出直接寫入既有 HTML canvas,
1259
+ 沒有增加 PNG、base64 或 JS 像素讀回。上方命令可開啟點擊換色的獨立示範。
1260
+ 已用原版 Omastorm 地圖與雷達 QSB 確認 CPU 繪圖;大型拼接圖的
1261
+ TextureCheck 與全部 UI 行為尚未驗證。自訂 vertex shader、mesh/culling、完整 GLSL 與其他
1262
+ QSB 版本仍未支援。純軟體是必要路徑,GPU 僅作未來可選加速。
1263
+
1264
+ ### Item capture and Canvas image reads
1265
+
1266
+ Local QML items support `grabToImage(callback, size)` using the existing DOM
1267
+ snapshot and HTML canvas path. The result exposes a PNG data URL; encoding is
1268
+ performed only for an explicit grab, never for each ShaderEffect frame.
1269
+ Canvas supports `loadImage`, `unloadImage`, `isImageLoaded`, `isImageLoading`,
1270
+ `isImageError`, `imageLoaded`, and the `createImageData(url)` image-read overload.
1271
+ Pending captures are cancelled on item destruction. `saveToFile` is not yet
1272
+ implemented. A sampler-size shader → 16×16 grab → Canvas image read integration
1273
+ test verifies green pixels for the actual 2×3 source texture, without a GPU.
1274
+
1275
+ The original Omastorm application still explicitly rejects `GraphicsInfo.Software`;
1276
+ software shader support alone cannot bypass that application policy. The explicit
1277
+ test-copy procedure below removes those policy checks without faking a backend.
1278
+
1279
+ ### Item 擷取與 Canvas 圖片讀取
1280
+
1281
+ 本地 QML Item 的 `grabToImage(callback, size)` 重用既有 DOM snapshot 與 HTML
1282
+ canvas,回傳 PNG data URL。只有明確擷取才編碼,不增加每幀 ShaderEffect 的成本。
1283
+ Canvas 已支援載入、卸載、載入狀態查詢、`imageLoaded` 及
1284
+ `createImageData(url)` 讀取圖片。Item 銷毀會取消待執行擷取;`saveToFile` 尚未實作。
1285
+ 整合測試已驗證「材質尺寸 shader → 16×16 擷取 → Canvas 讀取」取得正確綠色像素,
1286
+ 確認原始 2×3 材質尺寸,完全不需要 GPU。
1287
+
1288
+ Omastorm 原版仍明確拒絕 `GraphicsInfo.Software`,這是應用自己的限制,不能只靠
1289
+ 軟體 shader 支援解除。下方明確修改測試副本的做法會移除這兩處政策限制,
1290
+ 沒有假報繪圖後端。
1291
+
1292
+
1293
+ ### Omastorm software application experiment
1294
+
1295
+ With the official engine already running and Land available through
1296
+ `WAYLAND_DISPLAY` / `XDG_RUNTIME_DIR`:
1297
+
1298
+ ```sh
1299
+ bun test/manual-omastorm-software.mjs ../omastorm
1300
+ ```
1301
+
1302
+ The script prints a new temporary directory, copies only the UI, and removes
1303
+ the two explicit `GraphicsInfo.Software` rejection branches. It leaves the
1304
+ checkout, native shaders, texture-size verification and minimum window size
1305
+ unchanged. `--prepare-only` creates the copy without opening a window.
1306
+
1307
+ The browser now supplies Button content/background/padding/focus handling,
1308
+ attached Window changes, ListModel required delegate roles, locale/date helpers,
1309
+ and size/vector defaults. `FloatingWindow.minimumSize` and `maximumSize` affect
1310
+ QML geometry and are sent as xdg_toplevel size hints by the Wayland frontend.
1311
+ Only one frontend toplevel is currently represented; independent native windows
1312
+ and all Qt Control styling semantics are not claimed. Rectangle antialiasing
1313
+ uses the existing CSS/CanvasKit raster policy (disabling it is not implemented).
1314
+
1315
+ Nested Loader startup no longer loses pending outer change handlers. Imperative
1316
+ `createObject` initialization no longer adds child-state dependencies to its
1317
+ calling binding, which previously could recreate sockets repeatedly.
1318
+
1319
+ The full UI has displayed cached KMUX polar radar echoes, map tiles and place
1320
+ labels using software rendering. Missing icon glyphs, rendering cost, custom
1321
+ cursor visuals, mosaic grids and the full set of interactions still need work.
1322
+ This is an experiment, not complete Omastorm compatibility.
1323
+
1324
+ ### Omastorm 軟體版應用實驗
1325
+
1326
+ 先啟動官方引擎及 Land,設定 `WAYLAND_DISPLAY`/`XDG_RUNTIME_DIR`,再執行上方
1327
+ 命令。腳本會印出新的暫存目錄,只複製 UI、移除兩處 Software 拒絕判斷,不修改
1328
+ checkout、shader、材質尺寸驗證或最小視窗尺寸。`--prepare-only` 只建立副本。
1329
+
1330
+ 已補 Button 的內容、背景、padding 與焦點,Window 附加通知、ListModel 必填角色、
1331
+ 地區/日期格式及 size/向量初值。`minimumSize`/`maximumSize` 除了約束 QML
1332
+ 幾何,也會傳送 Wayland 尺寸提示。目前 frontend 仍只有單一原生 toplevel,
1333
+ 不是完整多視窗或全部 Qt Controls 樣式相容。Rectangle 抗鋸齒沿用現有 renderer,
1334
+ 尚未實作關閉抗鋸齒。
1335
+
1336
+ 已修正巢狀 Loader 啟動遺失外層變更通知,以及 `createObject` 建立過程污染呼叫方
1337
+ binding 依賴、造成 Socket 重建的問題。完整介面已用軟體顯示 KMUX 極座標雷達
1338
+ 回波、地圖與地名;圖示缺字、繪製成本、自訂游標視覺、拼接圖及全部互動仍待驗證。
1339
+
1340
+ ### Local fallback fonts / 本機備用字型
1341
+
1342
+ Local QML documents in host mode accept `QT_QPA_FONTDIR`. It names **one
1343
+ folder**, as in Qt; colon/semicolon-separated folder lists are not supported.
1344
+ This implementation additionally searches subfolders and follows symlinks with
1345
+ cycle/duplicate protection. Put fonts from several sources under one folder
1346
+ (or link their folders into it).
1347
+
1348
+ ```sh
1349
+ env QT_QPA_FONTDIR=/path/to/fonts \
1350
+ bun index.js --quickshell-wayland -p /path/to/shell.qml
1351
+ ```
1352
+
1353
+ Supported files are `.ttf` and `.otf`. Fonts are decoded once on the first local
1354
+ QML navigation per renderer process, then reused; there is no per-frame scan or
1355
+ automatic download. Existing fonts remain first, and the discovered fonts supply
1356
+ missing glyphs to both Text and Canvas. Regular faces are ordered before other
1357
+ faces. Explicit family names are also recognized, but complete Qt font style
1358
+ matching is not implemented. Private-use icon codepoints require the appropriate
1359
+ icon font (for example Nerd Font); another font may use the same codepoint for a
1360
+ different symbol.
1361
+
1362
+ The directory is read from the environment at process startup. Restart after
1363
+ changing its contents. No variable means no extra font scan. Ordinary HTML pages
1364
+ retain their existing fonts, including after navigation away from QML. Invalid
1365
+ fonts are reported and skipped. Loading is bounded to 32 MiB per file, 128 MiB
1366
+ total, 128 decoded faces, 4096 visited paths and 16 directory levels. Font
1367
+ collections (`.ttc`), Fontconfig rules and automatic system discovery are not
1368
+ implemented here.
1369
+
1370
+ 本機 QML 的 host 模式支援 `QT_QPA_FONTDIR`,與 Qt 一樣指定**單一目錄**,
1371
+ 不接受冒號或分號分隔的多目錄清單。我們另外支援遞迴子目錄及符號連結,會避免
1372
+ 循環和重複;多個字型來源可以放在同一目錄下,或把它們的目錄連結進去。
1373
+
1374
+ 支援 `.ttf`、`.otf`,每個 renderer 程序首次載入本機 QML 時解碼一次並重用,
1375
+ 不會每幀掃描或自動下載。原有字型優先,缺字才使用找到的字型,Text 與 Canvas
1376
+ 共用。Regular 字型排在其他樣式之前,也支援直接指定找到的 family 名稱,但尚未
1377
+ 完整實作 Qt 的字重/樣式匹配。私用區圖示需使用正確的圖示字型(例如 Nerd Font);
1378
+ 另一套字型在同一碼位可能是不同圖案。
1379
+
1380
+ 環境變數在程序啟動時讀取,修改目錄內容後請重啟。未設定就不額外掃描;一般 HTML
1381
+ 維持原本字型,即使從 QML 導航回 HTML 也一樣。損壞字型會回報並跳過。上限為每檔
1382
+ 32 MiB、合計 128 MiB、128 個解碼字型、4096 個路徑與 16 層目錄。此功能尚不支援
1383
+ `.ttc` 字型集合、Fontconfig 規則或自動搜尋系統字型。
1384
+
1385
+ Manual Text/Canvas icon check / 手動檢查文字及 Canvas 圖示:
1386
+
1387
+ ```sh
1388
+ env QT_QPA_FONTDIR=/path/to/nerd-fonts \
1389
+ bun index.js --quickshell-wayland -p test/fixtures/qml-fonts/main.qml
1390
+ ```
1391
+
1392
+ ### System locale / 系統地區設定
1393
+
1394
+ Local host-mode QML reads `LC_ALL`, then the relevant category (`LC_TIME`,
1395
+ `LC_MEASUREMENT`, or `LC_MESSAGES`), then `LANG`. Empty values are ignored.
1396
+ `C`, `C.UTF-8` and `POSIX` use the Qt C locale: metric units, day-first short
1397
+ dates and 24-hour time. Explicit `Qt.locale("en_US")` overrides the system
1398
+ categories; Unix names such as `en_US.UTF-8` are accepted. Unsupported locale
1399
+ names fall back to C. The environment is captured at process startup.
1400
+ Non-C format data and timezone labels still use JavaScript Intl, so exact
1401
+ punctuation or timezone abbreviations may differ from Qt's locale database.
1402
+ This does not change ordinary web-page Intl behavior.
1403
+
1404
+ 本機 host 模式的 QML 依序採用 `LC_ALL`、對應分類(`LC_TIME`、
1405
+ `LC_MEASUREMENT` 或 `LC_MESSAGES`)、`LANG`,空值跳過。
1406
+ `C`、`C.UTF-8` 和 `POSIX` 採 Qt 的 C locale:公制、日優先的短日期與
1407
+ 24 小時制。明確指定 `Qt.locale("en_US")` 時不受系統分類設定影響,也接受
1408
+ `en_US.UTF-8` 形式;不支援的名稱退回 C。環境設定在程序啟動時保存。
1409
+ 非 C 的格式資料及時區名稱仍使用 JavaScript Intl,因此標點或時區縮寫可能與
1410
+ Qt 資料庫不同。一般網頁的 Intl 行為不受影響。
1411
+
1412
+ ### Pointer actions and synchronous notifications / 滑鼠操作與同步通知
1413
+
1414
+ QML MouseArea and TapHandler now receive accepted right/middle clicks through
1415
+ both CDP/Casty and the Wayland frontend; double clicks also reach their handlers.
1416
+ Property `Changed.connect(...)` notifications run synchronously after binding
1417
+ invalidation, allowing callers to read mirrored state immediately. Ordinary
1418
+ rendering effects remain batched. This fixes Flea's Duplicate validation reading
1419
+ an obsolete zero selection count after its menu closes; it does not bypass the
1420
+ application's permission or selection checks. `Window.window.update()` requests
1421
+ a frame through the existing renderer; it does not create a separate render loop.
1422
+
1423
+ QML MouseArea/TapHandler 可從 CDP/Casty 與 Wayland 收到允許的右/中鍵,
1424
+ 雙擊也會傳給對應 handler。屬性的 `Changed.connect(...)` 在 binding 失效後同步
1425
+ 通知,讓呼叫端立刻讀到同步後的狀態;一般畫面更新仍批次處理。這修復 Flea
1426
+ 關閉選單後 Duplicate 驗證讀到舊的零選取數量,沒有跳過權限或選取檢查。
1427
+ `Window.window.update()` 透過既有 renderer 要求重繪,不另開繪圖迴圈。
@@ -27,7 +27,7 @@ export const nonvisualTypes = new Set([
27
27
  'Component',
28
28
  'Scale',
29
29
  'Translate',
30
- 'Repeater',
30
+ 'Shortcut', 'Instantiator', 'Repeater',
31
31
  'TapHandler',
32
32
  'HoverHandler',
33
33
  'QtObject',
@@ -35,6 +35,7 @@ export const nonvisualTypes = new Set([
35
35
  'Timer',
36
36
  'Connections',
37
37
  'FileView',
38
+ 'Socket',
38
39
  'Process',
39
40
  'StdioCollector',
40
41
  'SplitParser',
@@ -42,6 +43,8 @@ export const nonvisualTypes = new Set([
42
43
  'FontMetrics',
43
44
  ]);
44
45
  export const typeProperties = {
46
+ FloatingWindow:['minimumSize','maximumSize'],
47
+ Button:['padding','contentItem','background','focusPolicy','hovered','pressed','down'],
45
48
  MediaPlayer:['source','autoPlay','audioOutput','videoOutput','position','duration','playbackState','playing','mediaStatus','error','errorString','hasAudio','hasVideo','seekable','bufferProgress','loops','play','pause','stop'],
46
49
  AudioOutput:['muted','volume'],
47
50
  VideoOutput:['fillMode','sourceRect','contentRect'],
@@ -262,7 +265,7 @@ export const typeProperties = {
262
265
  'font',
263
266
  'color',
264
267
  'cursorPosition',
265
- 'cursorVisible',
268
+ 'cursorVisible','cursorDelegate',
266
269
  'selectionStart',
267
270
  'selectionEnd',
268
271
  'selectedText',
@@ -294,7 +297,7 @@ export const typeProperties = {
294
297
  'font',
295
298
  'color',
296
299
  'cursorPosition',
297
- 'cursorVisible',
300
+ 'cursorVisible','cursorDelegate',
298
301
  'selectionStart',
299
302
  'selectionEnd',
300
303
  'selectedText',
@@ -360,7 +363,9 @@ export const typeProperties = {
360
363
  Scale: ['xScale', 'yScale', 'origin'],
361
364
  Translate: ['x', 'y'],
362
365
  Shape: ['preferredRendererType', 'transform'],
363
- ShaderEffectSource: ['sourceItem', 'sourceRect', 'textureSize', 'hideSource', 'live', 'scheduleUpdate', 'scheduledUpdateCompleted'],
366
+ Canvas: ['loadImage','unloadImage','isImageLoaded','isImageLoading','isImageError','imageLoaded','available','contextType','context','canvasSize','getContext','requestPaint','markDirty','paint','painted','requestAnimationFrame','cancelRequestAnimationFrame'],
367
+ ShaderEffect: ['fragmentShader','vertexShader','status','log','blending','cullMode'],
368
+ ShaderEffectSource: ['mipmap','sourceItem', 'sourceRect', 'textureSize', 'hideSource', 'live', 'scheduleUpdate', 'scheduledUpdateCompleted'],
364
369
  ShapePath: [
365
370
  'fillItem',
366
371
  'fillGradient',
@@ -379,8 +384,12 @@ export const typeProperties = {
379
384
  Flow: ['flow', 'spacing', 'layoutDirection'],
380
385
  RowLayout: ['spacing', 'layoutDirection', 'uniformCellSizes'],
381
386
  ColumnLayout: ['spacing', 'uniformCellSizes'],
387
+ GridLayout: ['columns','rows','rowSpacing','columnSpacing'],
388
+ PathMultiline: ['paths'],
382
389
  Grid: ['columns', 'rows', 'spacing', 'rowSpacing', 'columnSpacing'],
383
390
 
391
+ Shortcut: ['sequence','sequences','enabled','autoRepeat','context','portableText','nativeText','activated','activatedAmbiguously'],
392
+ Instantiator: ['model','delegate','count','active','asynchronous','object','objectAt','objectAdded','objectRemoved'],
384
393
  Repeater: ['model', 'delegate', 'count', 'itemAt'],
385
394
  MouseArea: [
386
395
  'acceptedButtons',
@@ -432,6 +441,7 @@ export const typeProperties = {
432
441
  'fileChanged',
433
442
  'loadFailed',
434
443
  ],
444
+ Socket: ['path', 'connected', 'parser', 'write', 'flush', 'error'],
435
445
  Process: [
436
446
  'running',
437
447
  'command',
@@ -477,7 +487,12 @@ for (const type of ['ListView', 'GridView'])
477
487
  typeProperties[type].push(...typeProperties.Flickable);
478
488
 
479
489
  // Serialized into the page together with the reactive runtime.
480
- export function createQt() {
490
+ export function createQt(localeEnvironment = null) {
491
+ // Only local QML has the host capability. Never expose process.env to a
492
+ // sandboxed page; accept an explicit snapshot for deterministic tests.
493
+ const localeEnv = localeEnvironment ?? Object.fromEntries(
494
+ ['LC_ALL','LC_MESSAGES','LC_TIME','LC_MEASUREMENT','LANG'].map(key =>
495
+ [key, globalThis.__qmlHost?.env(key) || '']));
481
496
  const bindingValues = new WeakMap();
482
497
  const clamp = (x) => Math.max(0, Math.min(1, Number(x)));
483
498
  function rgba(r, g, b, a = 1) {
@@ -607,7 +622,60 @@ export function createQt() {
607
622
  for (let i = 0; i < 26; i++) keys[String.fromCharCode(65 + i)] = 65 + i;
608
623
  for (let i = 0; i < 10; i++) keys[String(i)] = 48 + i;
609
624
  for (let i = 1; i <= 35; i++) keys['F' + i] = 0x01000030 + i - 1;
625
+ function localeTag(name) {
626
+ const raw = String(name || 'C');
627
+ const base = raw.split('.')[0].split('@')[0];
628
+ if (/^(C|POSIX)$/i.test(base)) return 'C';
629
+ try {
630
+ const tag = Intl.getCanonicalLocales(base.replaceAll('_','-'))[0];
631
+ return Intl.DateTimeFormat.supportedLocalesOf([tag]).length ? tag : 'C';
632
+ } catch { return 'C'; }
633
+ }
634
+ function systemLocale(category) {
635
+ return localeTag(localeEnv.LC_ALL || localeEnv[category] || localeEnv.LANG || 'C');
636
+ }
637
+ function locale(name) {
638
+ const explicit = name !== undefined && name !== '';
639
+ const actual = explicit ? localeTag(name) : systemLocale('LC_MESSAGES');
640
+ const timeLocale = explicit ? actual : systemLocale('LC_TIME');
641
+ const measurement = explicit ? actual : systemLocale('LC_MEASUREMENT');
642
+ const region = measurement === 'C' ? '' : new Intl.Locale(measurement).maximize().region;
643
+ const pattern = (time, short) => {
644
+ if (timeLocale === 'C') return time ? (short ? 'HH:mm' : 'HH:mm:ss t')
645
+ : (short ? 'd MM yyyy' : 'dddd, d MMMM yyyy');
646
+ const formatter = new Intl.DateTimeFormat(timeLocale, time
647
+ ? {hour:'numeric',minute:'2-digit',...(short?{}:{second:'2-digit'})}
648
+ : {dateStyle:short?'short':'full'});
649
+ const twelve = ['h11','h12'].includes(formatter.resolvedOptions().hourCycle);
650
+ return formatter.formatToParts(new Date(2024,10,23,17,45,36)).map(p => {
651
+ switch(p.type) {
652
+ case 'year': return p.value.length === 2 ? 'yy' : 'yyyy';
653
+ case 'month': return /^\d+$/.test(p.value) ? (p.value.length === 2 ? 'MM' : 'M') : 'MMMM';
654
+ case 'day': return p.value.length === 2 ? 'dd' : 'd';
655
+ case 'weekday': return 'dddd';
656
+ case 'hour': return twelve ? 'h' : 'HH';
657
+ case 'minute': return 'mm';
658
+ case 'second': return 'ss';
659
+ case 'dayPeriod': return 'AP';
660
+ default: return /[A-Za-z']/.test(p.value) ? "'"+p.value.replaceAll("'","''")+"'" : p.value;
661
+ }
662
+ }).join('');
663
+ };
664
+ return {name:actual.replaceAll('-','_'),measurementSystem:region==='US'?1:region==='GB'?2:0,
665
+ timeFormat:format=>pattern(true,format===1),dateFormat:format=>pattern(false,format===1)};
666
+ }
667
+ function formatDateTime(input,format='ddd MMM d yyyy HH:mm:ss') {
668
+ const d=input instanceof Date?input:new Date(input);if(!Number.isFinite(d.getTime()))return '';
669
+ const pad=(n,width=2)=>String(n).padStart(width,'0'),hour=/AP|ap/.test(format)?d.getHours()%12||12:d.getHours();
670
+ const localTag=systemLocale('LC_TIME'), intlTag=localTag==='C'?'en-US':localTag;
671
+ const names=(options)=>new Intl.DateTimeFormat(intlTag,options).format(d);
672
+ const values={yyyy:d.getFullYear(),yy:pad(d.getFullYear()%100),MMMM:names({month:'long'}),MMM:names({month:'short'}),MM:pad(d.getMonth()+1),M:d.getMonth()+1,dddd:names({weekday:'long'}),ddd:names({weekday:'short'}),dd:pad(d.getDate()),d:d.getDate(),HH:pad(d.getHours()),H:d.getHours(),hh:pad(hour),h:hour,mm:pad(d.getMinutes()),m:d.getMinutes(),ss:pad(d.getSeconds()),s:d.getSeconds(),zzz:pad(d.getMilliseconds(),3),z:d.getMilliseconds(),AP:d.getHours()<12?'AM':'PM',ap:d.getHours()<12?'am':'pm',t:new Intl.DateTimeFormat(undefined,{timeZoneName:'short'}).formatToParts(d).find(p=>p.type==='timeZoneName')?.value||''};
673
+ return String(format).replace(/'((?:[^']|'')*)'|yyyy|MMMM|dddd|zzz|MMM|ddd|yy|MM|dd|HH|hh|mm|ss|AP|ap|[MdHhmszt]/g,(token,literal)=>literal!==undefined?literal.replaceAll("''", "'"):String(values[token]));
674
+ }
610
675
  return {
676
+ locale,formatDateTime,formatTime:(date,format="HH:mm:ss")=>formatDateTime(date,format),formatDate:(date,format="ddd MMM d yyyy")=>formatDateTime(date,format),
677
+ NoFocus:0, TabFocus:1, ClickFocus:2, StrongFocus:3,
678
+ WindowShortcut: 1, ApplicationShortcut: 2,
611
679
  ...Object.fromEntries(
612
680
  Object.entries(keys).map(([name, value]) => ['Key_' + name, value]),
613
681
  ),
@@ -634,6 +702,9 @@ export function createQt() {
634
702
  y = color(b);
635
703
  return ['r', 'g', 'b', 'a'].every((k) => Math.abs(x[k] - y[k]) < 1 / 255);
636
704
  },
705
+ vector2d: (x,y)=>({x:Number(x),y:Number(y)}),
706
+ vector3d: (x,y,z)=>({x:Number(x),y:Number(y),z:Number(z)}),
707
+ vector4d: (x,y,z,w)=>({x:Number(x),y:Number(y),z:Number(z),w:Number(w)}),
637
708
  point: (x, y) => ({ x, y }),
638
709
  size: (width, height) => ({ width, height }),
639
710
  rect: (x, y, width, height) => ({ x, y, width, height }),