buninu-browser 0.2.4 → 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 +102 -2
- 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 +57 -0
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +78 -13
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +5 -1
- package/lib/paint/frame.js +35 -3
- package/lib/paint/raw-frames.js +47 -0
- 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/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +25 -33
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/host.js +128 -92
- package/lib/renderer/main.js +38 -4
- package/lib/renderer/page-realm.js +32 -1
- package/lib/renderer/page-renderer.js +250 -21
- 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 +54 -0
- package/test/fixtures/browser-raw-demo.html +36 -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/fixtures/raw-capture-benchmark.json +76 -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/raw-capture.test.js +152 -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/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -0
|
@@ -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,22 @@ 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
|
+
},
|
|
1607
|
+
clipboardWrite: async text => {
|
|
1608
|
+
try {
|
|
1609
|
+
const url = new URL(document.URL);
|
|
1610
|
+
const secure = url.protocol === 'https:' || (url.protocol === 'http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname));
|
|
1611
|
+
if (!secure || !hooks.clipboardWrite) throw Error('Clipboard unavailable');
|
|
1612
|
+
await hooks.clipboardWrite(text);
|
|
1613
|
+
}
|
|
1614
|
+
catch { throw new DOMException('Clipboard write was not allowed or failed', 'NotAllowedError'); }
|
|
1615
|
+
},
|
|
1593
1616
|
sendBeacon: (url, data) => {
|
|
1594
1617
|
network.fetch(url, { method: "POST", body: data, mode: "no-cors", credentials: "include", keepalive: true })
|
|
1595
1618
|
.catch(() => {});
|
|
@@ -2672,7 +2695,7 @@ function navigatorPlatform(userAgent) {
|
|
|
2672
2695
|
* global exposes for them. Desktop Chromium lists five PDF viewer plugins
|
|
2673
2696
|
* and their two MIME types whatever is installed (HTML §8.9.1.6).
|
|
2674
2697
|
*/
|
|
2675
|
-
function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon }) {
|
|
2698
|
+
function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon, clipboardWrite, clipboardRead }) {
|
|
2676
2699
|
const EventTargetPrototype = eventTargets.EventTarget.prototype;
|
|
2677
2700
|
// An instance an event target's listeners can be added to.
|
|
2678
2701
|
const eventTarget = (prototype) => {
|
|
@@ -2750,6 +2773,13 @@ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile
|
|
|
2750
2773
|
}, targetOptions);
|
|
2751
2774
|
const NavigatorUAData = userAgentDataMembers ? define("NavigatorUAData", userAgentDataMembers) : null;
|
|
2752
2775
|
const languages = Object.freeze(["en-US"]);
|
|
2776
|
+
const Clipboard = define('Clipboard', {
|
|
2777
|
+
readText() { return clipboardRead(); },
|
|
2778
|
+
writeText(text) {
|
|
2779
|
+
if (arguments.length===0) return Promise.reject(new TypeError('writeText requires text'));
|
|
2780
|
+
return clipboardWrite(String(text));
|
|
2781
|
+
},
|
|
2782
|
+
}, targetOptions);
|
|
2753
2783
|
const Navigator = define("Navigator", {
|
|
2754
2784
|
userAgent,
|
|
2755
2785
|
appCodeName: "Mozilla",
|
|
@@ -2775,6 +2805,7 @@ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile
|
|
|
2775
2805
|
...(NavigatorUAData ? { userAgentData: NavigatorUAData.instance } : {}),
|
|
2776
2806
|
connection: NetworkInformation.instance,
|
|
2777
2807
|
permissions: Permissions.instance,
|
|
2808
|
+
clipboard: Clipboard.instance,
|
|
2778
2809
|
sendBeacon(url, data = null) { return sendBeacon(url, data); },
|
|
2779
2810
|
javaEnabled() { return false; },
|
|
2780
2811
|
getGamepads() { return [null, null, null, null]; },
|
|
@@ -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";
|
|
@@ -42,7 +54,7 @@ import { absolutizeStyleSheetUrls, extractFontFaces, matchSelectors, mediaQueryM
|
|
|
42
54
|
import { compileSelector, matchesCompiled } from "../happy-dom/selector-matcher.js";
|
|
43
55
|
import { elementFeatures, sheetTraits } from "../style/selector-traits.js";
|
|
44
56
|
import { buildDisplayList, visibleItems } from "../paint/display-list.js";
|
|
45
|
-
import { backgroundBytes, cropViewport, encodeImageBase64 } from "../paint/frame.js";
|
|
57
|
+
import { backgroundBytes, rawFrame, captureViewport, cropViewport, encodeImageBase64 } from "../paint/frame.js";
|
|
46
58
|
import { FrameCaptureCache } from "../paint/frame-cache.js";
|
|
47
59
|
import { RasterReadback } from "../paint/raster-readback.js";
|
|
48
60
|
import { insetRadii, roundedRect } from "../paint/rounded-rect.js";
|
|
@@ -227,6 +239,10 @@ 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")),
|
|
245
|
+
clipboardWrite = () => Promise.reject(Error("Clipboard unavailable")),
|
|
230
246
|
webSockets = null,
|
|
231
247
|
audio = null,
|
|
232
248
|
setCookie = () => {},
|
|
@@ -504,7 +520,7 @@ function buildPageRenderer({
|
|
|
504
520
|
const paragraph = makeParagraph(text, computedStyle);
|
|
505
521
|
paragraph.layout(100_000);
|
|
506
522
|
// getLongestLine() drops trailing white space, which line breaking needs.
|
|
507
|
-
width = paragraph.getMaxIntrinsicWidth();
|
|
523
|
+
width = {width:paragraph.getMaxIntrinsicWidth(),ascent:paragraph.getAlphabeticBaseline(),height:paragraph.getHeight()};
|
|
508
524
|
paragraph.delete();
|
|
509
525
|
if (++measuredCount > MAX_MEASURED_TEXTS) {
|
|
510
526
|
measuredWidths.clear();
|
|
@@ -513,7 +529,7 @@ function buildPageRenderer({
|
|
|
513
529
|
}
|
|
514
530
|
widths.set(text, width);
|
|
515
531
|
}
|
|
516
|
-
return width;
|
|
532
|
+
return width.width;
|
|
517
533
|
}
|
|
518
534
|
|
|
519
535
|
// Computed styles are immutable and shared, so each one's key is computed once.
|
|
@@ -533,8 +549,12 @@ function buildPageRenderer({
|
|
|
533
549
|
}
|
|
534
550
|
|
|
535
551
|
const fontMetricsCache = new Map();
|
|
536
|
-
function fontMetrics(computedStyle = null) {
|
|
552
|
+
function fontMetrics(computedStyle = null, text = null) {
|
|
537
553
|
const key = fontKeyOf(computedStyle);
|
|
554
|
+
if(text) {
|
|
555
|
+
measureText(text,computedStyle);
|
|
556
|
+
return measuredWidths.get(key).get(text);
|
|
557
|
+
}
|
|
538
558
|
let metrics = fontMetricsCache.get(key);
|
|
539
559
|
if (!metrics) {
|
|
540
560
|
const paragraph = makeParagraph("Hg", computedStyle);
|
|
@@ -787,18 +807,22 @@ function buildPageRenderer({
|
|
|
787
807
|
frameTimer = hostSetTimeout(() => {
|
|
788
808
|
frameTimer = null;
|
|
789
809
|
refresh();
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
}, Math.max(
|
|
810
|
+
// Coalesce ordinary mutations for 16ms. Expensive style/layout rebuilds
|
|
811
|
+
// retain a longer idle window so related script mutations can settle
|
|
812
|
+
// instead of continuously rebuilding a large document.
|
|
813
|
+
}, Math.max(16, lastRebuildMs * 2));
|
|
794
814
|
}
|
|
795
815
|
|
|
796
816
|
/** The viewport as an encoded image (base64), cut from the latest tile. */
|
|
797
|
-
async function viewportPng({ format = "png", quality = 80, optimizeForSpeed = false } = {}) {
|
|
817
|
+
async function viewportPng({ format = "png", quality = 80, optimizeForSpeed = false, clip } = {}) {
|
|
798
818
|
if (!lastTile) return "";
|
|
799
|
-
|
|
819
|
+
if (format === 'rgba' || format === 'bgra') {
|
|
820
|
+
const {pixels,width,height} = captureViewport(lastTile, viewport.x, viewport.y, page.width, page.height,clip);
|
|
821
|
+
return rawFrame(pixels,width,height,format);
|
|
822
|
+
}
|
|
823
|
+
const key = `${tileSerial}:${viewport.x}:${viewport.y}:${page.width}:${page.height}:${format}:${quality}:${optimizeForSpeed}:${JSON.stringify(clip)}`;
|
|
800
824
|
return captures.get(key, () => {
|
|
801
|
-
const { pixels, width, height } =
|
|
825
|
+
const { pixels, width, height } = captureViewport(lastTile, viewport.x, viewport.y, page.width, page.height,clip);
|
|
802
826
|
return encodeImageBase64(pixels, width, height, { format, quality, optimizeForSpeed });
|
|
803
827
|
});
|
|
804
828
|
}
|
|
@@ -1008,7 +1032,7 @@ function buildPageRenderer({
|
|
|
1008
1032
|
} else if (item.op === "drawSvg") {
|
|
1009
1033
|
drawSvg(item);
|
|
1010
1034
|
} else if (item.op === "drawText") {
|
|
1011
|
-
const entry = paragraphForPaint(item, scale, pixel(Math.max(1,
|
|
1035
|
+
const entry = paragraphForPaint(item, scale, pixel(Math.max(1, item.textWidth + 1)));
|
|
1012
1036
|
try {
|
|
1013
1037
|
if (item.gradient) {
|
|
1014
1038
|
// background-clip:text: draw glyphs, then keep the gradient only where they are.
|
|
@@ -1378,6 +1402,14 @@ function buildPageRenderer({
|
|
|
1378
1402
|
const traits = selectorTraitsFor(sheets);
|
|
1379
1403
|
const records = styleChanges.records;
|
|
1380
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
|
+
|
|
1381
1413
|
const roots = styleChanges.roots;
|
|
1382
1414
|
// Explicit roots mean that state-dependent selectors may need restyling;
|
|
1383
1415
|
// they do not by themselves change render-tree content. DOM mutation
|
|
@@ -1544,12 +1576,16 @@ function buildPageRenderer({
|
|
|
1544
1576
|
|
|
1545
1577
|
function replaceParsedDocument(parsed, styleSheets = [], imageResources = emptyImageResources(), linkSheets = null) {
|
|
1546
1578
|
if (page.document) loadImageResource.cancelDocument(page.document);
|
|
1579
|
+
resetInputCaret();
|
|
1547
1580
|
rasterReadback.dispose();
|
|
1548
1581
|
clearPaintParagraphs();
|
|
1549
1582
|
resetDocumentFonts();
|
|
1550
1583
|
disposeFrames();
|
|
1551
1584
|
closePiano();
|
|
1552
1585
|
disposeVideos();
|
|
1586
|
+
// qml handle begin: dispose application resources with their document
|
|
1587
|
+
page.document?.__qmlHost?.dispose();
|
|
1588
|
+
// qml handle end
|
|
1553
1589
|
page.realm?.dispose();
|
|
1554
1590
|
page.realm = null;
|
|
1555
1591
|
page.window?.happyDOM?.close?.();
|
|
@@ -1792,8 +1828,23 @@ function buildPageRenderer({
|
|
|
1792
1828
|
trace(`${url}: ${name} ${Math.round(now - phaseStart)}ms`);
|
|
1793
1829
|
phaseStart = now;
|
|
1794
1830
|
} : () => {};
|
|
1795
|
-
|
|
1796
|
-
|
|
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);
|
|
1797
1848
|
phase("parse");
|
|
1798
1849
|
// Stylesheets block rendering and scripts; images do not (only the
|
|
1799
1850
|
// window load event waits for them, see whenImagesLoaded): one slow image
|
|
@@ -1853,6 +1904,33 @@ function buildPageRenderer({
|
|
|
1853
1904
|
mode: page.scripting,
|
|
1854
1905
|
hooks: realmHooks(generation),
|
|
1855
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
|
+
|
|
1856
1934
|
await page.realm.runDocumentScripts();
|
|
1857
1935
|
if (!current()) return;
|
|
1858
1936
|
phase("scripts");
|
|
@@ -1952,7 +2030,9 @@ function buildPageRenderer({
|
|
|
1952
2030
|
/** Brings style, layout and raster up to date; returns whether it repainted. */
|
|
1953
2031
|
function refresh() {
|
|
1954
2032
|
if (disposedRenderer) return false;
|
|
2033
|
+
syncInputCaret();
|
|
1955
2034
|
if (page.dirty) rebuildRenderTree();
|
|
2035
|
+
if(followCaret) { followCaret=false; revealInputCaret(); }
|
|
1956
2036
|
if (!page.needsPaint) return false;
|
|
1957
2037
|
render();
|
|
1958
2038
|
return true;
|
|
@@ -1973,7 +2053,111 @@ function buildPageRenderer({
|
|
|
1973
2053
|
if (!prevented && element.tagName === "DETAILS") element.open = !element.open;
|
|
1974
2054
|
}
|
|
1975
2055
|
|
|
1976
|
-
|
|
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') {
|
|
1977
2161
|
const element = page.document.activeElement;
|
|
1978
2162
|
if (!element || !["INPUT", "TEXTAREA"].includes(element.tagName) || element.disabled || element.readOnly) return;
|
|
1979
2163
|
if (element.tagName === "INPUT" && !["text", "search", "url", "email", "tel", "password"].includes(element.type)) return;
|
|
@@ -1988,14 +2172,14 @@ function buildPageRenderer({
|
|
|
1988
2172
|
if (page.realm) {
|
|
1989
2173
|
const prevented = page.realm.dispatch(element, new page.window.InputEvent("beforeinput", {
|
|
1990
2174
|
bubbles: true, cancelable: true, data: backspace ? null : text,
|
|
1991
|
-
inputType: backspace ? "deleteContentBackward" :
|
|
2175
|
+
inputType: backspace ? "deleteContentBackward" : inputType,
|
|
1992
2176
|
}));
|
|
1993
2177
|
if (prevented) return;
|
|
1994
2178
|
}
|
|
1995
2179
|
element.value = value.slice(0, start) + text + value.slice(end);
|
|
1996
2180
|
markStyleDirty(element); // :placeholder-shown, :valid / :invalid
|
|
1997
2181
|
element.setSelectionRange?.(start + text.length, start + text.length);
|
|
1998
|
-
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 }));
|
|
1999
2183
|
page.dirty = true;
|
|
2000
2184
|
}
|
|
2001
2185
|
|
|
@@ -2059,6 +2243,20 @@ function buildPageRenderer({
|
|
|
2059
2243
|
canvasImage(element) {
|
|
2060
2244
|
if (!element) return null;
|
|
2061
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
|
+
|
|
2062
2260
|
const resource = page.resources.get(element);
|
|
2063
2261
|
if (!resource?.image) return null;
|
|
2064
2262
|
const url = resource.url ?? element.src ?? "";
|
|
@@ -2191,6 +2389,8 @@ function buildPageRenderer({
|
|
|
2191
2389
|
page.cookie = mergeDocumentCookie(page.cookie, String(value));
|
|
2192
2390
|
setCookie(String(value));
|
|
2193
2391
|
},
|
|
2392
|
+
clipboardRead,
|
|
2393
|
+
clipboardWrite,
|
|
2194
2394
|
webSockets,
|
|
2195
2395
|
pageFetch(request) {
|
|
2196
2396
|
const networkPending = pageFetch(request);
|
|
@@ -2437,6 +2637,7 @@ function buildPageRenderer({
|
|
|
2437
2637
|
* visualViewport.scale.
|
|
2438
2638
|
*/
|
|
2439
2639
|
resize(width, height, deviceScaleFactor = 1, pageScale = 1) {
|
|
2640
|
+
const resized = width !== page.width || height !== page.height;
|
|
2440
2641
|
page.pageScale = Number.isFinite(pageScale) && pageScale > 0 ? pageScale : 1;
|
|
2441
2642
|
page.width = width;
|
|
2442
2643
|
page.height = height;
|
|
@@ -2445,6 +2646,10 @@ function buildPageRenderer({
|
|
|
2445
2646
|
: 1;
|
|
2446
2647
|
viewport.resize(width, height);
|
|
2447
2648
|
rebuildRenderTree();
|
|
2649
|
+
// Listeners must observe the new viewport and layout. A repeated metrics
|
|
2650
|
+
// request (or DPR-only change) does not resize the CSS viewport.
|
|
2651
|
+
if (resized && page.realm)
|
|
2652
|
+
page.realm.dispatch(page.window, new page.window.Event("resize"));
|
|
2448
2653
|
render();
|
|
2449
2654
|
},
|
|
2450
2655
|
screenshot(options) {
|
|
@@ -2549,7 +2754,8 @@ function buildPageRenderer({
|
|
|
2549
2754
|
};
|
|
2550
2755
|
const prevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keydown", init));
|
|
2551
2756
|
let activated = false;
|
|
2552
|
-
|
|
2757
|
+
const inputHandled=!prevented&&inputDefault(target,init);
|
|
2758
|
+
if (!inputHandled && !prevented && !init.ctrlKey && !init.metaKey && !init.altKey) {
|
|
2553
2759
|
if ((key === "Enter" || key === " ") && isDisclosureControl(target)) {
|
|
2554
2760
|
activated = true;
|
|
2555
2761
|
if (key === "Enter") activateDisclosure(target);
|
|
@@ -2568,7 +2774,7 @@ function buildPageRenderer({
|
|
|
2568
2774
|
}
|
|
2569
2775
|
const upPrevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keyup", init));
|
|
2570
2776
|
if (activated && key === " " && !upPrevented) activateDisclosure(target);
|
|
2571
|
-
if (refresh() || prevented || activated) return { x: viewport.x, y: viewport.y };
|
|
2777
|
+
if (refresh() || prevented || activated || inputHandled) return { x: viewport.x, y: viewport.y };
|
|
2572
2778
|
}
|
|
2573
2779
|
const pageStep = Math.max(31, viewport.height - 31);
|
|
2574
2780
|
const delta = {
|
|
@@ -2655,7 +2861,7 @@ function buildPageRenderer({
|
|
|
2655
2861
|
// and a search form's forwarded field takes the click on the form.
|
|
2656
2862
|
const hitNode = page.displayList ? renderNodeAtPoint(page.renderTree, page.displayList, x, y, viewport) : null;
|
|
2657
2863
|
const precise = hitNode?.domNode?.nodeType === 1 ? hitNode.domNode : hitNode?.domNode?.parentElement;
|
|
2658
|
-
const target = forwarded ? region.element : precise ?? region?.element ?? null;
|
|
2864
|
+
const target = forwarded ? region.element : inputOwner.get(precise) ?? precise ?? region?.element ?? null;
|
|
2659
2865
|
const generatedDisclosure = hitNode?.pseudo === "summary" && target?.tagName === "DETAILS";
|
|
2660
2866
|
if (!target) {
|
|
2661
2867
|
focusedFrame = null;
|
|
@@ -2685,7 +2891,11 @@ function buildPageRenderer({
|
|
|
2685
2891
|
// The press focuses the focusable element it lands in, unless mousedown was canceled.
|
|
2686
2892
|
const focus = () => {
|
|
2687
2893
|
const focusable = generatedDisclosure ? target : target.closest?.("input, textarea, select, button, summary, [tabindex]");
|
|
2688
|
-
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
|
+
}
|
|
2689
2899
|
};
|
|
2690
2900
|
if (!page.realm) focus();
|
|
2691
2901
|
else {
|
|
@@ -2800,12 +3010,16 @@ function buildPageRenderer({
|
|
|
2800
3010
|
},
|
|
2801
3011
|
dispose() {
|
|
2802
3012
|
if (disposedRenderer) return;
|
|
3013
|
+
resetInputCaret();
|
|
2803
3014
|
disposedRenderer = true;
|
|
2804
3015
|
loadGeneration++;
|
|
2805
3016
|
if (page.document) loadImageResource.cancelDocument(page.document);
|
|
2806
3017
|
hostClearTimeout(prepaintTimer);
|
|
2807
3018
|
hostClearTimeout(frameTimer);
|
|
2808
3019
|
disposeFrames();
|
|
3020
|
+
// qml handle begin: renderer shutdown closes application resources
|
|
3021
|
+
page.document?.__qmlHost?.dispose();
|
|
3022
|
+
// qml handle end
|
|
2809
3023
|
page.realm?.dispose();
|
|
2810
3024
|
closePiano();
|
|
2811
3025
|
disposeVideos();
|
|
@@ -2969,6 +3183,10 @@ function buildPageRenderer({
|
|
|
2969
3183
|
requestDownload,
|
|
2970
3184
|
reportError,
|
|
2971
3185
|
pageFetch,
|
|
3186
|
+
clipboardRead,
|
|
3187
|
+
clipboardWrite,
|
|
3188
|
+
clipboardReadInput,
|
|
3189
|
+
clipboardWriteInput,
|
|
2972
3190
|
webSockets,
|
|
2973
3191
|
setCookie,
|
|
2974
3192
|
consoleMessage,
|
|
@@ -3129,6 +3347,12 @@ function buildPageRenderer({
|
|
|
3129
3347
|
if (resource) page.resources.set(node.domNode, resource);
|
|
3130
3348
|
else page.resources.delete(node.domNode);
|
|
3131
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
|
+
|
|
3132
3356
|
return buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf);
|
|
3133
3357
|
}
|
|
3134
3358
|
|
|
@@ -4399,6 +4623,11 @@ function buildPageRenderer({
|
|
|
4399
4623
|
sound.history.shift();
|
|
4400
4624
|
}
|
|
4401
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
|
+
|
|
4402
4631
|
audio.write(sound.id, pcm);
|
|
4403
4632
|
sent += pcm.length;
|
|
4404
4633
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { computeElementStyle } from '../style/computed-style.js';
|
|
2
|
+
export const inputPresentation = new WeakMap();
|
|
3
|
+
export const inputOwner = new WeakMap();
|
|
4
|
+
|
|
5
|
+
const spans = new WeakMap();
|
|
6
|
+
const carets = new WeakMap();
|
|
7
|
+
export const inputCaretNode = element => carets.get(element);
|
|
8
|
+
|
|
9
|
+
// Form controls share native selection and caret presentation across frontends.
|
|
10
|
+
export function textInputSelection(element, text, style, make) {
|
|
11
|
+
const options=inputPresentation.get(element)||{};
|
|
12
|
+
const focused=element.ownerDocument.activeElement===element;
|
|
13
|
+
const value=String(element.value??'');
|
|
14
|
+
const position=at=>element.type==='password'?[...value.slice(0,at)].length:at;
|
|
15
|
+
const start=position(element.selectionStart??0),end=position(element.selectionEnd??0);
|
|
16
|
+
const raw=(focused||options.persistent)&&end>start ? {start,end,background:options.background||'#3874f2',color:options.selectedColor||'white'}:null;
|
|
17
|
+
const caretAt = (at, selected=false) => {
|
|
18
|
+
let span=carets.get(element);
|
|
19
|
+
if(!span){span=element.ownerDocument.createElement('span');carets.set(element,span);inputOwner.set(span,element);}
|
|
20
|
+
const height=options.height||parseFloat(style.fontSize)||16;
|
|
21
|
+
const color=options.color||style.color;
|
|
22
|
+
const on=!selected&&options.on!==false&&options.visible!==false;
|
|
23
|
+
span.style.cssText=`display:inline-block;width:0;height:${height}px;border-left:1px solid ${on?color:'transparent'};margin-right:-1px;vertical-align:text-bottom;pointer-events:none`;
|
|
24
|
+
return make('caret','inline-block',{tagName:'SPAN',style:computeElementStyle(span,style),children:Object.freeze([])},span);
|
|
25
|
+
};
|
|
26
|
+
if (!raw) {
|
|
27
|
+
if(!focused||element.disabled||options.visible===false)return null;
|
|
28
|
+
const node=(part,text)=>make(part,'text',{text,style,valueStart:part==='after'?start:0,pseudo:'value',children:Object.freeze([])},element);
|
|
29
|
+
return [node('before',text.slice(0,start)),caretAt(start),node('after',text.slice(start))];
|
|
30
|
+
}
|
|
31
|
+
const { background, color } = raw;
|
|
32
|
+
const from = Math.max(0, Math.min(text.length, start)),
|
|
33
|
+
to = Math.max(from, Math.min(text.length, end));
|
|
34
|
+
if (from === to) return null;
|
|
35
|
+
const textNode = (part, text, textStyle = style, domNode = element) => make(part, 'text', {
|
|
36
|
+
text, style: textStyle, valueStart:part==='selected'?from:part==='after'?to:0, pseudo: 'value', children: Object.freeze([]),
|
|
37
|
+
}, domNode);
|
|
38
|
+
let span = spans.get(element);
|
|
39
|
+
if (!span) { span = element.ownerDocument.createElement('span'); spans.set(element, span); inputOwner.set(span,element); }
|
|
40
|
+
span.style.pointerEvents = 'none';
|
|
41
|
+
span.style.backgroundColor = background;
|
|
42
|
+
span.style.color = color;
|
|
43
|
+
const selectedStyle = computeElementStyle(span, style);
|
|
44
|
+
const children = [];
|
|
45
|
+
if (from) children.push(textNode('before', text.slice(0, from)));
|
|
46
|
+
if(focused&&element.selectionDirection==='backward')children.push(caretAt(from,true));
|
|
47
|
+
children.push(make('selection', 'inline', {
|
|
48
|
+
tagName: 'SPAN', style: selectedStyle,
|
|
49
|
+
children: Object.freeze([textNode('selected', text.slice(from, to), selectedStyle, span)]),
|
|
50
|
+
}, span));
|
|
51
|
+
if(focused&&element.selectionDirection!=='backward')children.push(caretAt(to,true));
|
|
52
|
+
if (to < text.length) children.push(textNode('after', text.slice(to)));
|
|
53
|
+
return children;
|
|
54
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Shared editing defaults for HTML and QML's real input/textarea elements.
|
|
2
|
+
export function isTextInput(element) {
|
|
3
|
+
return !!element && !element.disabled && (element.tagName==='TEXTAREA' ||
|
|
4
|
+
element.tagName==='INPUT' && ['text','search','url','email','tel','password'].includes(element.type));
|
|
5
|
+
}
|
|
6
|
+
export const inputRange = element => ({text:String(element.value),start:element.selectionStart??0,end:element.selectionEnd??0});
|
|
7
|
+
export function selectInput(element,start,end=start) {
|
|
8
|
+
element.setSelectionRange(Math.min(start,end),Math.max(start,end),end<start?'backward':'forward');
|
|
9
|
+
}
|
|
10
|
+
const columns=new WeakMap();
|
|
11
|
+
export function moveInputSelection(element,event) {
|
|
12
|
+
if(!isTextInput(element)||event.altKey)return false;
|
|
13
|
+
const {text,start,end}=inputRange(element),ctrl=event.ctrlKey||event.metaKey,key=event.key;
|
|
14
|
+
if(ctrl&&key.toLowerCase()==='a'){selectInput(element,0,text.length);return true;}
|
|
15
|
+
if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home','End'].includes(key))return false;
|
|
16
|
+
let at=element.selectionDirection==='backward'?start:end;
|
|
17
|
+
const anchor=at===start?end:start,multi=element.tagName==='TEXTAREA';
|
|
18
|
+
const lineStart=text.lastIndexOf('\n',at-1)+1;
|
|
19
|
+
if(key==='ArrowUp'||key==='ArrowDown') {
|
|
20
|
+
if(!multi)return false;
|
|
21
|
+
const previous=columns.get(element),column=previous?.at===at?previous.column:at-lineStart;
|
|
22
|
+
if(key==='ArrowUp') {
|
|
23
|
+
const stop=lineStart-1;at=stop<0?0:Math.min(text.lastIndexOf('\n',stop-1)+1+column,stop);
|
|
24
|
+
} else {
|
|
25
|
+
const next=text.indexOf('\n',at),stop=next<0?-1:text.indexOf('\n',next+1);
|
|
26
|
+
at=next<0?text.length:Math.min(next+1+column,stop<0?text.length:stop);
|
|
27
|
+
}
|
|
28
|
+
columns.set(element,{at,column});
|
|
29
|
+
} else {
|
|
30
|
+
columns.delete(element);
|
|
31
|
+
if(key==='Home')at=multi&&!ctrl?lineStart:0;
|
|
32
|
+
else if(key==='End'){const stop=text.indexOf('\n',at);at=multi&&!ctrl&&stop>=0?stop:text.length;}
|
|
33
|
+
else if(!event.shiftKey&&start!==end)at=key==='ArrowLeft'?start:end;
|
|
34
|
+
else if(key==='ArrowLeft') {
|
|
35
|
+
at-=[...text.slice(0,at)].pop()?.length??0;
|
|
36
|
+
if(ctrl){while(at>0&&/\s/u.test(text[at]))at--;while(at>0&&/[\p{L}\p{N}_]/u.test(text[at-1]))at--;}
|
|
37
|
+
} else {
|
|
38
|
+
at+=text.codePointAt(at)>0xffff?2:at<text.length?1:0;
|
|
39
|
+
if(ctrl){while(at<text.length&&/[\p{L}\p{N}_]/u.test(text[at]))at++;while(at<text.length&&/\s/u.test(text[at]))at++;}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
selectInput(element,event.shiftKey?anchor:at,at);return true;
|
|
43
|
+
}
|
|
44
|
+
export async function inputClipboard(element,operation,{read,write,replace}) {
|
|
45
|
+
if(!isTextInput(element)||operation!=='copy'&&element.readOnly)return;
|
|
46
|
+
const range=inputRange(element);
|
|
47
|
+
if(operation==='copy'||operation==='cut') {
|
|
48
|
+
if(element.type==='password'||range.start===range.end)return;
|
|
49
|
+
await write(range.text.slice(range.start,range.end));
|
|
50
|
+
if(operation==='copy')return;
|
|
51
|
+
}
|
|
52
|
+
let text=operation==='paste'?await read():'';
|
|
53
|
+
if(String(element.value)!==range.text||element.selectionStart!==range.start||element.selectionEnd!==range.end||element.readOnly||!element.isConnected)return;
|
|
54
|
+
if(element.tagName!=='TEXTAREA')text=text.replace(/[\r\n]+/g,'');
|
|
55
|
+
if(element.maxLength>=0)text=text.slice(0,Math.max(0,element.maxLength-(range.text.length-range.end+range.start)));
|
|
56
|
+
replace(text,operation==='paste'?'insertFromPaste':'deleteByCut');
|
|
57
|
+
}
|