buninu-browser 0.3.0 → 0.4.0
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.
- package/LICENSES/jsmdcui.txt +22 -0
- package/README.md +19 -4
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +123 -0
- package/frontends/wayland/casty/LICENSE +21 -0
- package/frontends/wayland/casty/bookmarks.js +32 -0
- package/frontends/wayland/casty/click.js +96 -0
- package/frontends/wayland/casty/commands.js +61 -0
- package/frontends/wayland/casty/config.js +45 -0
- package/frontends/wayland/casty/download-directory.js +28 -0
- package/frontends/wayland/casty/download.js +49 -0
- package/frontends/wayland/casty/hints.js +334 -0
- package/frontends/wayland/casty/keys.js +56 -0
- package/frontends/wayland/casty/page-commands.js +80 -0
- package/frontends/wayland/casty/prompt-width.js +148 -0
- package/frontends/wayland/casty/urlbar.js +606 -0
- package/frontends/wayland/client.js +88 -0
- package/frontends/wayland/clipboard.js +80 -0
- package/frontends/wayland/index.js +116 -0
- package/frontends/wayland/input.js +56 -0
- package/frontends/wayland/protocol.js +55 -0
- package/frontends/wayland/protocols/wayland.xml +3248 -0
- package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
- package/frontends/wayland/transport.js +120 -0
- package/frontends/wayland/urlbar.js +158 -0
- package/index.js +28 -1
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +37 -2
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +47 -8
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +5 -1
- package/lib/qml/README.md +971 -0
- package/lib/qml/builtins.js +700 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/document.js +1176 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +324 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +517 -0
- package/lib/qml/media-renderer.js +49 -0
- package/lib/qml/media.js +86 -0
- package/lib/qml/models.js +622 -0
- package/lib/qml/parser.js +381 -0
- package/lib/qml/qs +5 -0
- package/lib/qml/qscasty +3 -0
- package/lib/qml/qsheadless +3 -0
- package/lib/qml/qswayland +3 -0
- package/lib/qml/reactive.js +159 -0
- package/lib/qml/runtime.js +3381 -0
- package/lib/qml/shapes.js +243 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +123 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +11 -0
- package/lib/renderer/canvas-2d.js +13 -0
- package/lib/renderer/host.js +31 -6
- package/lib/renderer/main.js +19 -6
- package/lib/renderer/page-realm.js +16 -1
- package/lib/renderer/page-renderer.js +230 -13
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +2 -2
- package/package.json +1 -1
- package/test/clipboard.test.js +19 -0
- package/test/fixtures/flea-helpers/Dockerfile +36 -0
- package/test/fixtures/flea-helpers/bat.mjs +43 -0
- package/test/fixtures/flea-helpers/bin/bat +3 -0
- package/test/fixtures/flea-helpers/bin/bwrap +10 -0
- package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
- package/test/fixtures/flea-helpers/bin/glow +3 -0
- package/test/fixtures/flea-helpers/bin/qs +3 -0
- package/test/fixtures/flea-helpers/bin/unsupported +3 -0
- package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
- package/test/fixtures/flea-helpers/check-media.mjs +29 -0
- package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
- package/test/fixtures/flea-helpers/check.mjs +36 -0
- package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
- package/test/fixtures/flea-helpers/source-mime.xml +28 -0
- package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
- package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
- package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
- package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea/LICENSE +21 -0
- package/test/fixtures/qml/flea/README.md +25 -0
- package/test/fixtures/qml/flea/Theme.qml +8 -0
- package/test/fixtures/qml/flea/demo.qml +24 -0
- package/test/fixtures/qml/flea/qmldir +3 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
- package/test/fixtures/qml/flea-theme/README.md +70 -0
- package/test/fixtures/qml/flea-theme/button.qml +17 -0
- package/test/fixtures/qml/flea-theme/demo.qml +27 -0
- package/test/fixtures/qml/flea-theme/field.qml +21 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
- package/test/fixtures/qml/flea-theme/segment.qml +17 -0
- package/test/fixtures/qml/flea-theme/settings.qml +58 -0
- package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
- package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
- package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
- package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
- package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
- package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
- package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
- package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
- package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
- package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
- package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
- package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
- package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
- package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
- package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
- package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
- package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
- package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
- package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
- package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
- package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
- package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
- package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
- package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
- package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
- package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
- package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
- package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
- package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
- package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
- package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
- package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
- package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
- package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
- package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
- package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
- package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
- package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
- package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
- package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
- package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
- package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
- package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
- package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
- package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
- package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
- package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
- package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
- package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
- package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
- package/test/fixtures/qml/hello.qml +26 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
- package/test/fixtures/qml/imports/components/Badge.qml +15 -0
- package/test/fixtures/qml/imports/logic/base.js +2 -0
- package/test/fixtures/qml/imports/logic/counter.js +4 -0
- package/test/fixtures/qml/imports/logic/math.js +3 -0
- package/test/fixtures/qml/imports/logic/shared.js +3 -0
- package/test/fixtures/qml/imports/main.qml +21 -0
- package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
- package/test/fixtures/qml/imports/theme/qmldir +2 -0
- package/test/fixtures/qml/multiply.qml +61 -0
- package/test/fixtures/qml-canvas-dom.js +90 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/helpers/flea-sandbox.js +48 -0
- package/test/helpers/quickshell-fixture.js +125 -0
- package/test/html-clipboard-wayland.test.js +47 -0
- package/test/index.test.js +13 -0
- package/test/manual-casty-without-qml.js +87 -0
- package/test/manual-flea-application.js +49 -0
- package/test/manual-flea-binary.js +62 -0
- package/test/manual-flea-qml.js +9 -0
- package/test/manual-flea-settings.js +40 -0
- package/test/manual-flea-theme.js +40 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-clipboard-bridge.test.js +25 -0
- package/test/qml-clipboard-wayland.test.js +40 -0
- package/test/qml-connections-timing.test.js +73 -0
- package/test/qml-dynamic.test.js +401 -0
- package/test/qml-flea-app.test.js +146 -0
- package/test/qml-flea-backend.test.js +78 -0
- package/test/qml-flea-columns-backend.test.js +38 -0
- package/test/qml-flea-compile.test.js +78 -0
- package/test/qml-flea-launcher.test.js +88 -0
- package/test/qml-flea-markdown-backend.test.js +31 -0
- package/test/qml-flea-media.test.js +28 -0
- package/test/qml-flea-navigation-backend.test.js +35 -0
- package/test/qml-flea-navigation-timing.test.js +147 -0
- package/test/qml-flea-preview-backend.test.js +53 -0
- package/test/qml-flea-previews.test.js +163 -0
- package/test/qml-flea-rows.test.js +101 -0
- package/test/qml-flea-textures.test.js +59 -0
- package/test/qml-flea-view-switch.test.js +33 -0
- package/test/qml-flea-wayland.test.js +68 -0
- package/test/qml-flea.test.js +34 -0
- package/test/qml-host.test.js +238 -0
- package/test/qml-imports.test.js +102 -0
- package/test/qml-inherited-handlers.test.js +51 -0
- package/test/qml-input-selection.test.js +66 -0
- package/test/qml-interactions.test.js +309 -0
- package/test/qml-language.test.js +167 -0
- package/test/qml-launcher.test.js +54 -0
- package/test/qml-layout-text.test.js +205 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-persisted-delegates.test.js +45 -0
- package/test/qml-quickshell-controls.test.js +65 -0
- package/test/qml-quickshell-settings.test.js +62 -0
- package/test/qml-repeater.test.js +72 -0
- package/test/qml-style-updates.test.js +124 -0
- package/test/qml-textures.test.js +66 -0
- package/test/qml-views.test.js +332 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/renderer-host.test.js +11 -0
- package/test/text-input-pointer.test.js +31 -0
- package/test/text-input-scroll.test.js +29 -0
- package/test/text-input.test.js +47 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-urlbar.test.js +38 -0
package/lib/renderer/host.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {writeClipboard} from '../clipboard.js';
|
|
1
|
+
import {writeClipboard, readClipboard} from '../clipboard.js';
|
|
2
2
|
/**
|
|
3
3
|
* Controller-side handle for a renderer process.
|
|
4
4
|
*
|
|
@@ -55,11 +55,13 @@ const LIFECYCLE_NAMES = new Set(["DOMContentLoaded", "load"]);
|
|
|
55
55
|
const DEFAULT_ENTRY = Bun.fileURLToPath(new URL("./main.js", import.meta.url));
|
|
56
56
|
|
|
57
57
|
export class RendererHost {
|
|
58
|
+
#clipboardRead = readClipboard;
|
|
58
59
|
#clipboardWrite = writeClipboard;
|
|
59
60
|
#clipboardUntil = 0;
|
|
60
61
|
#clipboardPending = false;
|
|
61
62
|
#rawFrames = null;
|
|
62
63
|
#startEpoch = 0;
|
|
64
|
+
#closed = false;
|
|
63
65
|
#rejectStartup = null;
|
|
64
66
|
#entry;
|
|
65
67
|
#args;
|
|
@@ -114,8 +116,10 @@ export class RendererHost {
|
|
|
114
116
|
network = new NetworkService({ fetch: fetchImpl }), onFrame = null, onLifecycle = null,
|
|
115
117
|
// A dedicated loopback socket works independently of the main thread on all platforms.
|
|
116
118
|
compositor = entry === DEFAULT_ENTRY,
|
|
119
|
+
clipboardRead = readClipboard,
|
|
117
120
|
clipboardWrite = writeClipboard,
|
|
118
121
|
} = {}) {
|
|
122
|
+
this.#clipboardRead = clipboardRead;
|
|
119
123
|
this.#clipboardWrite = clipboardWrite;
|
|
120
124
|
this.#onFrame = onFrame;
|
|
121
125
|
this.#onLifecycle = onLifecycle;
|
|
@@ -131,6 +135,7 @@ export class RendererHost {
|
|
|
131
135
|
|
|
132
136
|
/** Starts the renderer if it is not running and waits until it is ready. */
|
|
133
137
|
start() {
|
|
138
|
+
if (this.#closed) return Promise.reject(Error("Renderer is closed"));
|
|
134
139
|
if (this.#ready) return this.#ready;
|
|
135
140
|
let resolveReady;
|
|
136
141
|
let rejectReady;
|
|
@@ -149,13 +154,19 @@ export class RendererHost {
|
|
|
149
154
|
()=>this.#closeChannel(channel));
|
|
150
155
|
if (epoch !== this.#startEpoch) { channel?.transport.close(); throw Error('Renderer startup cancelled'); }
|
|
151
156
|
this.#channel = channel;
|
|
157
|
+
// No inherited environment: nothing from the controller leaks in.
|
|
158
|
+
// Windows programs need SystemRoot to load system libraries.
|
|
159
|
+
let env = process.platform === "win32" && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {};
|
|
160
|
+
|
|
161
|
+
// qml handle begin: applications need HOME/PATH/runtime paths, web pages do not
|
|
162
|
+
if (this.#args.includes("--buninu-qml-host")) env = { ...process.env };
|
|
163
|
+
// qml handle end
|
|
164
|
+
|
|
152
165
|
const child = Bun.spawn({
|
|
153
166
|
cmd: [process.execPath, this.#entry, ...this.#args,
|
|
154
167
|
...(logging ? [RENDERER_LOG_FLAG] : []), ...(compositor ? [COMPOSITOR_FLAG, `--compositor-endpoint=${JSON.stringify(channel.transport.endpoint)}`] : []),
|
|
155
168
|
...(process.env.BUNINU_VERIFY_STYLE ? [VERIFY_STYLE_FLAG] : []), ...(process.env.BUNINU_FULL_STYLE ? [FULL_STYLE_FLAG] : [])],
|
|
156
|
-
|
|
157
|
-
// Windows programs need SystemRoot to load system libraries.
|
|
158
|
-
env: process.platform === "win32" && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {},
|
|
169
|
+
env,
|
|
159
170
|
// With BUNINU_LOG, output goes through the controller into the log file.
|
|
160
171
|
stdio: ["ignore", logging ? "pipe" : "inherit", logging ? "pipe" : "inherit"],
|
|
161
172
|
serialization: "advanced",
|
|
@@ -297,6 +308,7 @@ export class RendererHost {
|
|
|
297
308
|
}
|
|
298
309
|
|
|
299
310
|
close() {
|
|
311
|
+
this.#closed = true;
|
|
300
312
|
this.#startEpoch++;
|
|
301
313
|
this.#rejectStartup?.(Error("Renderer closed during startup")); this.#rejectStartup=null;
|
|
302
314
|
this.#rawFrames?.close(); this.#rawFrames=null;
|
|
@@ -313,6 +325,7 @@ export class RendererHost {
|
|
|
313
325
|
}
|
|
314
326
|
|
|
315
327
|
async #call(method, params, timeout = this.#timeout) {
|
|
328
|
+
if (this.#closed) throw Error("Renderer is closed");
|
|
316
329
|
if (!this.#child) await this.#restart();
|
|
317
330
|
await this.start();
|
|
318
331
|
const child = this.#child;
|
|
@@ -365,11 +378,23 @@ export class RendererHost {
|
|
|
365
378
|
pending.reject(new Error(`renderer: ${String(message.error).split("\n")[0] || "unknown error"}`));
|
|
366
379
|
}
|
|
367
380
|
else pending.resolve(message.result);
|
|
368
|
-
} else if (
|
|
381
|
+
} else if (['clipboard-read','clipboard-read-input'].includes(message.type) && Number.isInteger(message.id)) {
|
|
382
|
+
const reply = (text, error) => {if(this.#child===child) child.send({type:'clipboard-result',id:message.id,text,error});};
|
|
383
|
+
let secure=false;
|
|
384
|
+
try {const url=new URL(this.#documentUrl); secure=url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname));} catch {}
|
|
385
|
+
if ((!secure && !message.type.endsWith('-input')) || Date.now()>this.#clipboardUntil || this.#clipboardPending) {
|
|
386
|
+
reply(undefined,'Clipboard read requires a recent user action in a secure document'); return;
|
|
387
|
+
}
|
|
388
|
+
this.#clipboardUntil=0; this.#clipboardPending=true;
|
|
389
|
+
Promise.resolve().then(()=>this.#clipboardRead()).then(text=>{
|
|
390
|
+
if(typeof text!=='string'||Buffer.byteLength(text)>1024*1024)throw Error('Clipboard text exceeds limit');
|
|
391
|
+
reply(text);
|
|
392
|
+
}).catch(()=>reply(undefined,'Unable to read system clipboard')).finally(()=>{this.#clipboardPending=false;});
|
|
393
|
+
} else if (['clipboard-write','clipboard-write-input'].includes(message.type) && Number.isInteger(message.id)) {
|
|
369
394
|
const reply = error => { if(this.#child===child)child.send({type:'clipboard-result',id:message.id,...(error?{error}: {})}); };
|
|
370
395
|
let secure=false;
|
|
371
396
|
try { const url=new URL(this.#documentUrl); secure=url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname)); } catch {}
|
|
372
|
-
if (!secure || Date.now()>this.#clipboardUntil || this.#clipboardPending
|
|
397
|
+
if ((!secure && !message.type.endsWith('-input')) || Date.now()>this.#clipboardUntil || this.#clipboardPending
|
|
373
398
|
|| typeof message.text!=='string' || Buffer.byteLength(message.text)>1024*1024) {
|
|
374
399
|
reply('Clipboard write requires a recent user action in a secure document'); return;
|
|
375
400
|
}
|
package/lib/renderer/main.js
CHANGED
|
@@ -241,12 +241,13 @@ async function startScriptRewriter() {
|
|
|
241
241
|
|
|
242
242
|
let nextClipboardId=1;
|
|
243
243
|
const clipboardWrites=new Map();
|
|
244
|
-
function clipboardWrite(
|
|
244
|
+
function clipboardRead() { return clipboardWrite(undefined, 'clipboard-read'); }
|
|
245
|
+
function clipboardWrite(text, type = 'clipboard-write') {
|
|
245
246
|
const id=nextClipboardId++;
|
|
246
247
|
return new Promise((resolve,reject)=>{
|
|
247
248
|
const timer=setTimeout(()=>{clipboardWrites.delete(id);reject(Error('Clipboard timed out'));},5000);
|
|
248
249
|
clipboardWrites.set(id,{resolve,reject,timer});
|
|
249
|
-
process.send({type
|
|
250
|
+
process.send({type,id,text});
|
|
250
251
|
});
|
|
251
252
|
}
|
|
252
253
|
|
|
@@ -256,7 +257,10 @@ const renderer = await createPageRenderer({
|
|
|
256
257
|
requestNavigation: (url, download = null, fragmentFrom = null, post = null) => process.send({ type: "navigate", url, download, fragmentFrom, post }),
|
|
257
258
|
requestDownload: ({ url, name, data }) => process.send({ type: "download-data", url, name, data }),
|
|
258
259
|
pageFetch,
|
|
260
|
+
clipboardRead,
|
|
259
261
|
clipboardWrite,
|
|
262
|
+
clipboardReadInput:()=>clipboardWrite(undefined,'clipboard-read-input'),
|
|
263
|
+
clipboardWriteInput:text=>clipboardWrite(text,'clipboard-write-input'),
|
|
260
264
|
webSockets,
|
|
261
265
|
audio,
|
|
262
266
|
setCookie: (value) => process.send({ type: "cookie", value }),
|
|
@@ -281,11 +285,20 @@ const renderer = await createPageRenderer({
|
|
|
281
285
|
videoDecoder: wasmpeg,
|
|
282
286
|
});
|
|
283
287
|
await warmUp();
|
|
284
|
-
|
|
288
|
+
let allowHostJs = process.argv.includes(DANGEROUSLY_ALLOW_HOST_JS);
|
|
289
|
+
let hostReason = `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}`;
|
|
290
|
+
|
|
291
|
+
// qml handle begin: controller-selected local QML application process
|
|
292
|
+
if (process.argv.includes("--buninu-qml-host")) {
|
|
293
|
+
allowHostJs = true;
|
|
294
|
+
hostReason = "local QML application renderer";
|
|
295
|
+
}
|
|
296
|
+
// qml handle end
|
|
297
|
+
|
|
285
298
|
const seccomp = allowHostJs
|
|
286
|
-
? { installed: false, reason:
|
|
299
|
+
? { installed: false, reason: hostReason }
|
|
287
300
|
: installSeccomp();
|
|
288
|
-
const ses = allowHostJs ? { installed: false, reason:
|
|
301
|
+
const ses = allowHostJs ? { installed: false, reason: hostReason } : lockdownRealm();
|
|
289
302
|
renderer.setScripting(allowHostJs ? "host" : ses.installed ? "ses" : "off");
|
|
290
303
|
|
|
291
304
|
/** Hardens the JavaScript realm so page scripts can run in SES compartments. */
|
|
@@ -340,7 +353,7 @@ process.on("message", async (message) => {
|
|
|
340
353
|
if(message?.type==='clipboard-result') {
|
|
341
354
|
const pending=clipboardWrites.get(message.id);if(!pending)return;
|
|
342
355
|
clipboardWrites.delete(message.id);clearTimeout(pending.timer);
|
|
343
|
-
if(message.error)pending.reject(Error(String(message.error)));else pending.resolve();
|
|
356
|
+
if(message.error)pending.reject(Error(String(message.error)));else pending.resolve(message.text);
|
|
344
357
|
return;
|
|
345
358
|
}
|
|
346
359
|
|
|
@@ -192,6 +192,13 @@ const INTERFACE_MEMBERS = {
|
|
|
192
192
|
alt: ["HTMLImageElement", "HTMLAreaElement", "HTMLInputElement"],
|
|
193
193
|
placeholder: ["HTMLInputElement", "HTMLTextAreaElement"],
|
|
194
194
|
readOnly: ["HTMLInputElement", "HTMLTextAreaElement"],
|
|
195
|
+
maxLength: ["HTMLInputElement", "HTMLTextAreaElement"],
|
|
196
|
+
selectionStart: ["HTMLInputElement", "HTMLTextAreaElement"],
|
|
197
|
+
selectionEnd: ["HTMLInputElement", "HTMLTextAreaElement"],
|
|
198
|
+
selectionDirection: ["HTMLInputElement", "HTMLTextAreaElement"],
|
|
199
|
+
setSelectionRange: ["HTMLInputElement", "HTMLTextAreaElement"],
|
|
200
|
+
setRangeText: ["HTMLInputElement", "HTMLTextAreaElement"],
|
|
201
|
+
select: ["HTMLInputElement", "HTMLTextAreaElement"],
|
|
195
202
|
required: ["HTMLInputElement", "HTMLTextAreaElement", "HTMLSelectElement"],
|
|
196
203
|
min: ["HTMLInputElement", "HTMLMeterElement"], max: ["HTMLInputElement", "HTMLMeterElement", "HTMLProgressElement"],
|
|
197
204
|
step: ["HTMLInputElement"],
|
|
@@ -1590,6 +1597,13 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1590
1597
|
const platform = platformObjects({
|
|
1591
1598
|
eventTargets, userAgent, userAgentDataMembers: chromeVersion ? userAgentDataMembers : null, mobile: mobileUserAgent, viewport: hooks.viewport,
|
|
1592
1599
|
performance: userTiming.performance, storages: { local: hostStorage.get(origin), session: new Map() },
|
|
1600
|
+
clipboardRead: async () => {
|
|
1601
|
+
try {
|
|
1602
|
+
const url=new URL(document.URL);
|
|
1603
|
+
if (!(url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname))) || !hooks.clipboardRead) throw Error('Clipboard unavailable');
|
|
1604
|
+
return await hooks.clipboardRead();
|
|
1605
|
+
} catch {throw new DOMException('Clipboard read was not allowed or failed','NotAllowedError');}
|
|
1606
|
+
},
|
|
1593
1607
|
clipboardWrite: async text => {
|
|
1594
1608
|
try {
|
|
1595
1609
|
const url = new URL(document.URL);
|
|
@@ -2681,7 +2695,7 @@ function navigatorPlatform(userAgent) {
|
|
|
2681
2695
|
* global exposes for them. Desktop Chromium lists five PDF viewer plugins
|
|
2682
2696
|
* and their two MIME types whatever is installed (HTML §8.9.1.6).
|
|
2683
2697
|
*/
|
|
2684
|
-
function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon, clipboardWrite }) {
|
|
2698
|
+
function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon, clipboardWrite, clipboardRead }) {
|
|
2685
2699
|
const EventTargetPrototype = eventTargets.EventTarget.prototype;
|
|
2686
2700
|
// An instance an event target's listeners can be added to.
|
|
2687
2701
|
const eventTarget = (prototype) => {
|
|
@@ -2760,6 +2774,7 @@ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile
|
|
|
2760
2774
|
const NavigatorUAData = userAgentDataMembers ? define("NavigatorUAData", userAgentDataMembers) : null;
|
|
2761
2775
|
const languages = Object.freeze(["en-US"]);
|
|
2762
2776
|
const Clipboard = define('Clipboard', {
|
|
2777
|
+
readText() { return clipboardRead(); },
|
|
2763
2778
|
writeText(text) {
|
|
2764
2779
|
if (arguments.length===0) return Promise.reject(new TypeError('writeText requires text'));
|
|
2765
2780
|
return clipboardWrite(String(text));
|
|
@@ -22,10 +22,22 @@
|
|
|
22
22
|
* then scripts run. `loadDocument` resolves at the first paint; `whenLoaded`
|
|
23
23
|
* after the load event.
|
|
24
24
|
*/
|
|
25
|
+
import {isTextInput, moveInputSelection, inputClipboard, selectInput} from './text-input.js';
|
|
26
|
+
import {inputPresentation, inputOwner, inputCaretNode} from './text-input-paint.js';
|
|
25
27
|
import CanvasKitInit from "../../usr/lib/canvaskit/canvaskit.js";
|
|
26
28
|
import { setCanvasKit, canvasResource, observeDocumentCanvases } from "./canvas-2d.js";
|
|
27
29
|
import { createCanvasTextRenderer, parseCanvasFont } from "./canvas-text.js";
|
|
28
30
|
import { DESKTOP_USER_AGENT } from "../user-agent.js";
|
|
31
|
+
|
|
32
|
+
// qml handle begin: optional document frontend
|
|
33
|
+
import {createQmlMediaController,applyQmlMediaGain} from '../qml/media-renderer.js';
|
|
34
|
+
import {INTERFACES as qmlBindings} from "./bindings.js";
|
|
35
|
+
import { enableQmlDomImages, hasQmlDomImages, captureQmlDomImage, filterQmlTextureSources, qmlTextureVisibilityChanged } from "../qml/canvas-dom.js";
|
|
36
|
+
import {createQmlHost} from "../qml/host.js";
|
|
37
|
+
import { loadQmlDocument } from "../qml/document.js";
|
|
38
|
+
import { isQmlDocument } from "../qml/format.js";
|
|
39
|
+
// qml handle end
|
|
40
|
+
|
|
29
41
|
import { parseHTMLDocument } from "../happy-dom/parser.js";
|
|
30
42
|
import { LayoutEngine } from "../layout/text-layout.js";
|
|
31
43
|
import { ScrollViewport } from "../layout/scroll-viewport.js";
|
|
@@ -227,6 +239,9 @@ function buildPageRenderer({
|
|
|
227
239
|
requestDownload = () => {},
|
|
228
240
|
reportError = () => {},
|
|
229
241
|
pageFetch = () => Promise.reject(new TypeError("network access is not available")),
|
|
242
|
+
clipboardReadInput = () => clipboardRead(),
|
|
243
|
+
clipboardWriteInput = text => clipboardWrite(text),
|
|
244
|
+
clipboardRead = () => Promise.reject(Error("Clipboard unavailable")),
|
|
230
245
|
clipboardWrite = () => Promise.reject(Error("Clipboard unavailable")),
|
|
231
246
|
webSockets = null,
|
|
232
247
|
audio = null,
|
|
@@ -505,7 +520,7 @@ function buildPageRenderer({
|
|
|
505
520
|
const paragraph = makeParagraph(text, computedStyle);
|
|
506
521
|
paragraph.layout(100_000);
|
|
507
522
|
// getLongestLine() drops trailing white space, which line breaking needs.
|
|
508
|
-
width = paragraph.getMaxIntrinsicWidth();
|
|
523
|
+
width = {width:paragraph.getMaxIntrinsicWidth(),ascent:paragraph.getAlphabeticBaseline(),height:paragraph.getHeight()};
|
|
509
524
|
paragraph.delete();
|
|
510
525
|
if (++measuredCount > MAX_MEASURED_TEXTS) {
|
|
511
526
|
measuredWidths.clear();
|
|
@@ -514,7 +529,7 @@ function buildPageRenderer({
|
|
|
514
529
|
}
|
|
515
530
|
widths.set(text, width);
|
|
516
531
|
}
|
|
517
|
-
return width;
|
|
532
|
+
return width.width;
|
|
518
533
|
}
|
|
519
534
|
|
|
520
535
|
// Computed styles are immutable and shared, so each one's key is computed once.
|
|
@@ -534,8 +549,12 @@ function buildPageRenderer({
|
|
|
534
549
|
}
|
|
535
550
|
|
|
536
551
|
const fontMetricsCache = new Map();
|
|
537
|
-
function fontMetrics(computedStyle = null) {
|
|
552
|
+
function fontMetrics(computedStyle = null, text = null) {
|
|
538
553
|
const key = fontKeyOf(computedStyle);
|
|
554
|
+
if(text) {
|
|
555
|
+
measureText(text,computedStyle);
|
|
556
|
+
return measuredWidths.get(key).get(text);
|
|
557
|
+
}
|
|
539
558
|
let metrics = fontMetricsCache.get(key);
|
|
540
559
|
if (!metrics) {
|
|
541
560
|
const paragraph = makeParagraph("Hg", computedStyle);
|
|
@@ -1013,7 +1032,7 @@ function buildPageRenderer({
|
|
|
1013
1032
|
} else if (item.op === "drawSvg") {
|
|
1014
1033
|
drawSvg(item);
|
|
1015
1034
|
} else if (item.op === "drawText") {
|
|
1016
|
-
const entry = paragraphForPaint(item, scale, pixel(Math.max(1,
|
|
1035
|
+
const entry = paragraphForPaint(item, scale, pixel(Math.max(1, item.textWidth + 1)));
|
|
1017
1036
|
try {
|
|
1018
1037
|
if (item.gradient) {
|
|
1019
1038
|
// background-clip:text: draw glyphs, then keep the gradient only where they are.
|
|
@@ -1383,6 +1402,14 @@ function buildPageRenderer({
|
|
|
1383
1402
|
const traits = selectorTraitsFor(sheets);
|
|
1384
1403
|
const records = styleChanges.records;
|
|
1385
1404
|
if (styleChanges.observer) records.push(...styleChanges.observer.takeRecords());
|
|
1405
|
+
|
|
1406
|
+
// qml handle begin: QML visual attributes invalidate retained rendering
|
|
1407
|
+
if (hasQmlDomImages(page.document) && qmlTextureVisibilityChanged(records)) {
|
|
1408
|
+
page.resourcePaintDirty = true;
|
|
1409
|
+
page.needsPaint = true;
|
|
1410
|
+
}
|
|
1411
|
+
// qml handle end
|
|
1412
|
+
|
|
1386
1413
|
const roots = styleChanges.roots;
|
|
1387
1414
|
// Explicit roots mean that state-dependent selectors may need restyling;
|
|
1388
1415
|
// they do not by themselves change render-tree content. DOM mutation
|
|
@@ -1549,12 +1576,16 @@ function buildPageRenderer({
|
|
|
1549
1576
|
|
|
1550
1577
|
function replaceParsedDocument(parsed, styleSheets = [], imageResources = emptyImageResources(), linkSheets = null) {
|
|
1551
1578
|
if (page.document) loadImageResource.cancelDocument(page.document);
|
|
1579
|
+
resetInputCaret();
|
|
1552
1580
|
rasterReadback.dispose();
|
|
1553
1581
|
clearPaintParagraphs();
|
|
1554
1582
|
resetDocumentFonts();
|
|
1555
1583
|
disposeFrames();
|
|
1556
1584
|
closePiano();
|
|
1557
1585
|
disposeVideos();
|
|
1586
|
+
// qml handle begin: dispose application resources with their document
|
|
1587
|
+
page.document?.__qmlHost?.dispose();
|
|
1588
|
+
// qml handle end
|
|
1558
1589
|
page.realm?.dispose();
|
|
1559
1590
|
page.realm = null;
|
|
1560
1591
|
page.window?.happyDOM?.close?.();
|
|
@@ -1797,8 +1828,23 @@ function buildPageRenderer({
|
|
|
1797
1828
|
trace(`${url}: ${name} ${Math.round(now - phaseStart)}ms`);
|
|
1798
1829
|
phaseStart = now;
|
|
1799
1830
|
} : () => {};
|
|
1800
|
-
|
|
1801
|
-
|
|
1831
|
+
|
|
1832
|
+
let isHtml = contentType.toLowerCase().includes("html");
|
|
1833
|
+
let parsed;
|
|
1834
|
+
|
|
1835
|
+
// qml handle begin: supply DOM before the default HTML parser
|
|
1836
|
+
if (isQmlDocument(url, contentType)) {
|
|
1837
|
+
const qmlHost = page.scripting === "host" && new URL(url).protocol === "file:" ? createQmlHost(url) : null;
|
|
1838
|
+
parsed = await loadQmlDocument(source, url, {scripting: page.scripting !== "off", fetchResource, importRoots:qmlHost?.importRoots});
|
|
1839
|
+
parsed.document.__qmlHost = qmlHost;
|
|
1840
|
+
if (qmlHost) enableQmlDomImages(parsed.document);
|
|
1841
|
+
if (!current()) {qmlHost?.dispose();return;}
|
|
1842
|
+
// Reuse the existing stylesheet/script/lifecycle pipeline for QML DOM.
|
|
1843
|
+
isHtml = true;
|
|
1844
|
+
}
|
|
1845
|
+
// qml handle end
|
|
1846
|
+
|
|
1847
|
+
parsed ??= parseHTMLDocument(isHtml ? source : "<body></body>", url, referrer);
|
|
1802
1848
|
phase("parse");
|
|
1803
1849
|
// Stylesheets block rendering and scripts; images do not (only the
|
|
1804
1850
|
// window load event waits for them, see whenImagesLoaded): one slow image
|
|
@@ -1858,6 +1904,33 @@ function buildPageRenderer({
|
|
|
1858
1904
|
mode: page.scripting,
|
|
1859
1905
|
hooks: realmHooks(generation),
|
|
1860
1906
|
});
|
|
1907
|
+
// qml handle begin: only a local QML host realm receives this capability
|
|
1908
|
+
if (page.document.__qmlHost) page.realm.pageGlobal.__qmlHost = page.document.__qmlHost;
|
|
1909
|
+
// qml handle end
|
|
1910
|
+
|
|
1911
|
+
// qml handle begin: QML methods/properties adapt shared media and text controls
|
|
1912
|
+
if(page.document.__qmlHost) {
|
|
1913
|
+
const host=page.document.__qmlHost;
|
|
1914
|
+
host.media=element=>{
|
|
1915
|
+
element=qmlBindings.Node.unwrap(element)??element;
|
|
1916
|
+
if(element?.ownerDocument!==page.document||!['VIDEO','AUDIO'].includes(element.tagName))throw Error('Invalid QML media element');
|
|
1917
|
+
return createQmlMediaController(element,{
|
|
1918
|
+
valid:e=>e.ownerDocument===page.document,state:e=>page.media.get(e),
|
|
1919
|
+
close:closeMedia,forget:e=>{page.media.delete(e);page.resources.delete(e);},ensure:mediaStateFor,
|
|
1920
|
+
load:loadMedia,seek:seekMedia,play:playMedia,pause:pauseMedia,
|
|
1921
|
+
snapshot:mediaState,error:mediaError,gains:qmlMediaGains,
|
|
1922
|
+
});
|
|
1923
|
+
};
|
|
1924
|
+
|
|
1925
|
+
host.inputClipboard=(element,operation)=>clipboardForInput(qmlBindings.Node.unwrap(element)??element,operation,{read:()=>host.clipboardRead(),write:text=>host.clipboardWrite(text)});
|
|
1926
|
+
host.inputPresentation=(element,options)=>{
|
|
1927
|
+
element=qmlBindings.Node.unwrap(element)??element;
|
|
1928
|
+
if(element?.ownerDocument!==page.document)return;
|
|
1929
|
+
inputPresentation.set(element,Object.assign(inputPresentation.get(element)||{},options));invalidateInput();
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
// qml handle end
|
|
1933
|
+
|
|
1861
1934
|
await page.realm.runDocumentScripts();
|
|
1862
1935
|
if (!current()) return;
|
|
1863
1936
|
phase("scripts");
|
|
@@ -1957,7 +2030,9 @@ function buildPageRenderer({
|
|
|
1957
2030
|
/** Brings style, layout and raster up to date; returns whether it repainted. */
|
|
1958
2031
|
function refresh() {
|
|
1959
2032
|
if (disposedRenderer) return false;
|
|
2033
|
+
syncInputCaret();
|
|
1960
2034
|
if (page.dirty) rebuildRenderTree();
|
|
2035
|
+
if(followCaret) { followCaret=false; revealInputCaret(); }
|
|
1961
2036
|
if (!page.needsPaint) return false;
|
|
1962
2037
|
render();
|
|
1963
2038
|
return true;
|
|
@@ -1978,7 +2053,111 @@ function buildPageRenderer({
|
|
|
1978
2053
|
if (!prevented && element.tagName === "DETAILS") element.open = !element.open;
|
|
1979
2054
|
}
|
|
1980
2055
|
|
|
1981
|
-
|
|
2056
|
+
// qml handle begin: optional per-player PCM gain
|
|
2057
|
+
const qmlMediaGains=new WeakMap();
|
|
2058
|
+
// qml handle end
|
|
2059
|
+
|
|
2060
|
+
let caretElement, caretKey, caretTimer, followCaret=false;
|
|
2061
|
+
function resetInputCaret() {
|
|
2062
|
+
hostClearInterval(caretTimer);caretTimer=null;caretElement=null;caretKey=null;
|
|
2063
|
+
}
|
|
2064
|
+
function invalidateInput() {
|
|
2065
|
+
page.resourceLayoutDirty=true;page.dirty=true;page.needsPaint=true;scheduleFrame();
|
|
2066
|
+
}
|
|
2067
|
+
function syncInputCaret() {
|
|
2068
|
+
const element=page.document?.activeElement;
|
|
2069
|
+
const valid=isTextInput(element)&&element.isConnected;
|
|
2070
|
+
const key=valid?`${element.selectionStart}:${element.selectionEnd}:${element.value}:${inputPresentation.get(element)?.visible}`:'';
|
|
2071
|
+
if(element===caretElement&&key===caretKey)return;
|
|
2072
|
+
resetInputCaret();caretElement=element;caretKey=key;followCaret=true;
|
|
2073
|
+
if(!valid)return;
|
|
2074
|
+
const options=inputPresentation.get(element)||{};options.on=true;inputPresentation.set(element,options);
|
|
2075
|
+
page.resourceLayoutDirty=true;page.dirty=true;page.needsPaint=true;
|
|
2076
|
+
if(element.selectionStart!==element.selectionEnd||options.visible===false)return;
|
|
2077
|
+
caretTimer=hostSetInterval(()=>{
|
|
2078
|
+
if(page.document?.activeElement!==element||!element.isConnected){resetInputCaret();invalidateInput();return;}
|
|
2079
|
+
options.on=!options.on;invalidateInput();
|
|
2080
|
+
},500);
|
|
2081
|
+
caretTimer.unref?.();
|
|
2082
|
+
}
|
|
2083
|
+
function revealInputCaret() {
|
|
2084
|
+
const element=page.document?.activeElement;
|
|
2085
|
+
if(!isTextInput(element))return;
|
|
2086
|
+
const position=element.selectionDirection==='backward'?element.selectionStart:element.selectionEnd;
|
|
2087
|
+
if(position===0){setElementScroll(element,0,0);return;}
|
|
2088
|
+
const caret=layoutBoxOf(inputCaretNode(element)),field=layoutBoxOf(element),geometry=scrollerGeometry(element);
|
|
2089
|
+
if(!caret||!field||!geometry)return;
|
|
2090
|
+
const [top,, ,left]=field.node.style.borderWidths||[0,0,0,0];
|
|
2091
|
+
const padding=field.node.style.padding||[0,0,0,0];
|
|
2092
|
+
const x=caret.box.x-field.box.x-left,y=caret.box.y-field.box.y-top;
|
|
2093
|
+
const px=Number(padding[3])||0,py=Number(padding[0])||0;
|
|
2094
|
+
let sx=geometry.x,sy=geometry.y;
|
|
2095
|
+
if(x<sx+px)sx=x-px;
|
|
2096
|
+
else if(x+1>sx+geometry.clientWidth-px)sx=x+1-geometry.clientWidth+px;
|
|
2097
|
+
if(y<sy+py)sy=y-py;
|
|
2098
|
+
else if(y+caret.box.height>sy+geometry.clientHeight-py)sy=y+caret.box.height-geometry.clientHeight+py;
|
|
2099
|
+
setElementScroll(element,sx,sy);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Hit the same laid-out/shaped runs that paint the control. This is only
|
|
2103
|
+
// performed on a pointer press, never on the animation/capture path.
|
|
2104
|
+
function inputPositionAtPoint(element,x,y) {
|
|
2105
|
+
if(!String(element.value??''))return 0;
|
|
2106
|
+
let best=null;
|
|
2107
|
+
for(const hit of page.displayList?.hits??[]) {
|
|
2108
|
+
const run=hit.textRun;if(!run)continue;
|
|
2109
|
+
const node=page.renderTree.nodesById.get(hit.nodeId);
|
|
2110
|
+
if((inputOwner.get(node?.domNode)??node?.domNode)!==element)continue;
|
|
2111
|
+
let px=x+(hit.fixed?0:viewport.x),py=y+(hit.fixed?0:viewport.y);
|
|
2112
|
+
if(hit.transform){
|
|
2113
|
+
const {a,d,e,f}=hit.transform;if(!a||!d)continue;
|
|
2114
|
+
px=(px-e)/a;py=(py-f)/d;
|
|
2115
|
+
}
|
|
2116
|
+
const top=run.y, height=parseFloat(run.style.lineHeight)||parseFloat(run.style.fontSize)||16;
|
|
2117
|
+
const dy=Math.max(top-py,py-top-height,0),dx=Math.max(run.x-px,px-run.x-run.width,0);
|
|
2118
|
+
if(!best||dy<best.dy||dy===best.dy&&dx<best.dx)best={run,node,px,dy,dx};
|
|
2119
|
+
}
|
|
2120
|
+
if(!best)return 0;
|
|
2121
|
+
const {run,node,px}=best;
|
|
2122
|
+
const paragraph=makeParagraph(run.text,run.style);let at;
|
|
2123
|
+
try{
|
|
2124
|
+
paragraph.layout(Math.max(1,run.width+1));
|
|
2125
|
+
at=paragraph.getGlyphPositionAtCoordinate(px-run.x,0).pos;
|
|
2126
|
+
}finally{paragraph.delete();}
|
|
2127
|
+
let position=(node.valueStart??0)+(run.sourceStart??0)+at;
|
|
2128
|
+
if(element.type==='password')position=[...String(element.value)].slice(0,position).join('').length;
|
|
2129
|
+
return Math.max(0,Math.min(String(element.value).length,position));
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
function clipboardForInput(element,operation,backend={read:clipboardReadInput,write:clipboardWriteInput}) {
|
|
2133
|
+
return inputClipboard(element,operation,{...backend,replace:(text,type)=>{
|
|
2134
|
+
if(page.document.activeElement!==element)return;
|
|
2135
|
+
editFocusedText(text,false,type);syncInputCaret();invalidateInput();
|
|
2136
|
+
}});
|
|
2137
|
+
}
|
|
2138
|
+
function inputDefault(element,event) {
|
|
2139
|
+
if(!isTextInput(element)||event.altKey)return false;
|
|
2140
|
+
if(moveInputSelection(element,event)){syncInputCaret();invalidateInput();return true;}
|
|
2141
|
+
if((event.ctrlKey||event.metaKey)&&['c','x','v'].includes(event.key.toLowerCase())) {
|
|
2142
|
+
let backend;
|
|
2143
|
+
|
|
2144
|
+
// qml handle begin: local applications choose their frontend clipboard transport
|
|
2145
|
+
const host=page.document.__qmlHost;
|
|
2146
|
+
if(host)backend={read:()=>host.clipboardRead(),write:text=>host.clipboardWrite(text)};
|
|
2147
|
+
// qml handle end
|
|
2148
|
+
|
|
2149
|
+
clipboardForInput(element,{c:'copy',x:'cut',v:'paste'}[event.key.toLowerCase()],backend).catch(reportError);
|
|
2150
|
+
return true;
|
|
2151
|
+
}
|
|
2152
|
+
if(event.key==='Delete'&&!element.readOnly) {
|
|
2153
|
+
const at=element.selectionStart;
|
|
2154
|
+
if(at===element.selectionEnd)selectInput(element,at,at+(String(element.value).codePointAt(at)>0xffff?2:1));
|
|
2155
|
+
editFocusedText('',false,'deleteContentForward');invalidateInput();return true;
|
|
2156
|
+
}
|
|
2157
|
+
return false;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
function editFocusedText(text, backspace = false, inputType = 'insertText') {
|
|
1982
2161
|
const element = page.document.activeElement;
|
|
1983
2162
|
if (!element || !["INPUT", "TEXTAREA"].includes(element.tagName) || element.disabled || element.readOnly) return;
|
|
1984
2163
|
if (element.tagName === "INPUT" && !["text", "search", "url", "email", "tel", "password"].includes(element.type)) return;
|
|
@@ -1993,14 +2172,14 @@ function buildPageRenderer({
|
|
|
1993
2172
|
if (page.realm) {
|
|
1994
2173
|
const prevented = page.realm.dispatch(element, new page.window.InputEvent("beforeinput", {
|
|
1995
2174
|
bubbles: true, cancelable: true, data: backspace ? null : text,
|
|
1996
|
-
inputType: backspace ? "deleteContentBackward" :
|
|
2175
|
+
inputType: backspace ? "deleteContentBackward" : inputType,
|
|
1997
2176
|
}));
|
|
1998
2177
|
if (prevented) return;
|
|
1999
2178
|
}
|
|
2000
2179
|
element.value = value.slice(0, start) + text + value.slice(end);
|
|
2001
2180
|
markStyleDirty(element); // :placeholder-shown, :valid / :invalid
|
|
2002
2181
|
element.setSelectionRange?.(start + text.length, start + text.length);
|
|
2003
|
-
if (page.realm) page.realm.dispatch(element, new page.window.
|
|
2182
|
+
if (page.realm) page.realm.dispatch(element, new page.window.InputEvent("input", { bubbles: true, inputType:backspace?"deleteContentBackward":inputType, data:backspace?null:text }));
|
|
2004
2183
|
page.dirty = true;
|
|
2005
2184
|
}
|
|
2006
2185
|
|
|
@@ -2064,6 +2243,20 @@ function buildPageRenderer({
|
|
|
2064
2243
|
canvasImage(element) {
|
|
2065
2244
|
if (!element) return null;
|
|
2066
2245
|
if (element.tagName === "CANVAS") return canvasResource(element);
|
|
2246
|
+
|
|
2247
|
+
// qml handle begin: local QML may draw a DOM subtree into a canvas
|
|
2248
|
+
if (hasQmlDomImages(page.document) && !["IMG", "VIDEO", "AUDIO", "IMAGE"].includes(element.tagName)) {
|
|
2249
|
+
if (page.dirty) rebuildRenderTree();
|
|
2250
|
+
return captureQmlDomImage(element, {
|
|
2251
|
+
document: page.document, tree: page.renderTree, layout: page.layout,
|
|
2252
|
+
resources: page.resources, backgroundResource, scrollOffsetOf,
|
|
2253
|
+
kit: CanvasKit, paint: paintDisplayList, canvasResource,
|
|
2254
|
+
isOriginClean: resource => resource?.originClean ?? Boolean(resource?.url &&
|
|
2255
|
+
(resource.url.startsWith("data:") || originOf(resource.url) === originOf(page.url))),
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
// qml handle end
|
|
2259
|
+
|
|
2067
2260
|
const resource = page.resources.get(element);
|
|
2068
2261
|
if (!resource?.image) return null;
|
|
2069
2262
|
const url = resource.url ?? element.src ?? "";
|
|
@@ -2196,6 +2389,7 @@ function buildPageRenderer({
|
|
|
2196
2389
|
page.cookie = mergeDocumentCookie(page.cookie, String(value));
|
|
2197
2390
|
setCookie(String(value));
|
|
2198
2391
|
},
|
|
2392
|
+
clipboardRead,
|
|
2199
2393
|
clipboardWrite,
|
|
2200
2394
|
webSockets,
|
|
2201
2395
|
pageFetch(request) {
|
|
@@ -2560,7 +2754,8 @@ function buildPageRenderer({
|
|
|
2560
2754
|
};
|
|
2561
2755
|
const prevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keydown", init));
|
|
2562
2756
|
let activated = false;
|
|
2563
|
-
|
|
2757
|
+
const inputHandled=!prevented&&inputDefault(target,init);
|
|
2758
|
+
if (!inputHandled && !prevented && !init.ctrlKey && !init.metaKey && !init.altKey) {
|
|
2564
2759
|
if ((key === "Enter" || key === " ") && isDisclosureControl(target)) {
|
|
2565
2760
|
activated = true;
|
|
2566
2761
|
if (key === "Enter") activateDisclosure(target);
|
|
@@ -2579,7 +2774,7 @@ function buildPageRenderer({
|
|
|
2579
2774
|
}
|
|
2580
2775
|
const upPrevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keyup", init));
|
|
2581
2776
|
if (activated && key === " " && !upPrevented) activateDisclosure(target);
|
|
2582
|
-
if (refresh() || prevented || activated) return { x: viewport.x, y: viewport.y };
|
|
2777
|
+
if (refresh() || prevented || activated || inputHandled) return { x: viewport.x, y: viewport.y };
|
|
2583
2778
|
}
|
|
2584
2779
|
const pageStep = Math.max(31, viewport.height - 31);
|
|
2585
2780
|
const delta = {
|
|
@@ -2666,7 +2861,7 @@ function buildPageRenderer({
|
|
|
2666
2861
|
// and a search form's forwarded field takes the click on the form.
|
|
2667
2862
|
const hitNode = page.displayList ? renderNodeAtPoint(page.renderTree, page.displayList, x, y, viewport) : null;
|
|
2668
2863
|
const precise = hitNode?.domNode?.nodeType === 1 ? hitNode.domNode : hitNode?.domNode?.parentElement;
|
|
2669
|
-
const target = forwarded ? region.element : precise ?? region?.element ?? null;
|
|
2864
|
+
const target = forwarded ? region.element : inputOwner.get(precise) ?? precise ?? region?.element ?? null;
|
|
2670
2865
|
const generatedDisclosure = hitNode?.pseudo === "summary" && target?.tagName === "DETAILS";
|
|
2671
2866
|
if (!target) {
|
|
2672
2867
|
focusedFrame = null;
|
|
@@ -2696,7 +2891,11 @@ function buildPageRenderer({
|
|
|
2696
2891
|
// The press focuses the focusable element it lands in, unless mousedown was canceled.
|
|
2697
2892
|
const focus = () => {
|
|
2698
2893
|
const focusable = generatedDisclosure ? target : target.closest?.("input, textarea, select, button, summary, [tabindex]");
|
|
2699
|
-
if (focusable && !focusable.disabled)
|
|
2894
|
+
if (focusable && !focusable.disabled) {
|
|
2895
|
+
const position=isTextInput(focusable)&&!forwarded?inputPositionAtPoint(focusable,x,y):null;
|
|
2896
|
+
focusable.focus();
|
|
2897
|
+
if(position!==null){selectInput(focusable,position);syncInputCaret();invalidateInput();}
|
|
2898
|
+
}
|
|
2700
2899
|
};
|
|
2701
2900
|
if (!page.realm) focus();
|
|
2702
2901
|
else {
|
|
@@ -2811,12 +3010,16 @@ function buildPageRenderer({
|
|
|
2811
3010
|
},
|
|
2812
3011
|
dispose() {
|
|
2813
3012
|
if (disposedRenderer) return;
|
|
3013
|
+
resetInputCaret();
|
|
2814
3014
|
disposedRenderer = true;
|
|
2815
3015
|
loadGeneration++;
|
|
2816
3016
|
if (page.document) loadImageResource.cancelDocument(page.document);
|
|
2817
3017
|
hostClearTimeout(prepaintTimer);
|
|
2818
3018
|
hostClearTimeout(frameTimer);
|
|
2819
3019
|
disposeFrames();
|
|
3020
|
+
// qml handle begin: renderer shutdown closes application resources
|
|
3021
|
+
page.document?.__qmlHost?.dispose();
|
|
3022
|
+
// qml handle end
|
|
2820
3023
|
page.realm?.dispose();
|
|
2821
3024
|
closePiano();
|
|
2822
3025
|
disposeVideos();
|
|
@@ -2980,7 +3183,10 @@ function buildPageRenderer({
|
|
|
2980
3183
|
requestDownload,
|
|
2981
3184
|
reportError,
|
|
2982
3185
|
pageFetch,
|
|
3186
|
+
clipboardRead,
|
|
2983
3187
|
clipboardWrite,
|
|
3188
|
+
clipboardReadInput,
|
|
3189
|
+
clipboardWriteInput,
|
|
2984
3190
|
webSockets,
|
|
2985
3191
|
setCookie,
|
|
2986
3192
|
consoleMessage,
|
|
@@ -3141,6 +3347,12 @@ function buildPageRenderer({
|
|
|
3141
3347
|
if (resource) page.resources.set(node.domNode, resource);
|
|
3142
3348
|
else page.resources.delete(node.domNode);
|
|
3143
3349
|
}
|
|
3350
|
+
|
|
3351
|
+
// qml handle begin: texture sources keep layout/input but paint through their texture
|
|
3352
|
+
if (hasQmlDomImages(page.document)) return filterQmlTextureSources(
|
|
3353
|
+
buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf), page.renderTree);
|
|
3354
|
+
// qml handle end
|
|
3355
|
+
|
|
3144
3356
|
return buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf);
|
|
3145
3357
|
}
|
|
3146
3358
|
|
|
@@ -4411,6 +4623,11 @@ function buildPageRenderer({
|
|
|
4411
4623
|
sound.history.shift();
|
|
4412
4624
|
}
|
|
4413
4625
|
const pcm = floatToS16(taken);
|
|
4626
|
+
|
|
4627
|
+
// qml handle begin: apply QML AudioOutput gain to the output chunk only
|
|
4628
|
+
applyQmlMediaGain(pcm,qmlMediaGains.get(state.element));
|
|
4629
|
+
// qml handle end
|
|
4630
|
+
|
|
4414
4631
|
audio.write(sound.id, pcm);
|
|
4415
4632
|
sent += pcm.length;
|
|
4416
4633
|
}
|