buninu-browser 0.3.0 → 0.5.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 +40 -4
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +152 -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 +130 -0
- package/frontends/wayland/input.js +75 -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 +162 -0
- package/index.js +28 -1
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- 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 +54 -12
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +6 -1
- package/lib/qml/README.md +1427 -0
- package/lib/qml/builtins.js +771 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +1199 -0
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +352 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +521 -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 +182 -0
- package/lib/qml/runtime.js +3603 -0
- package/lib/qml/shader/fragment.js +83 -0
- package/lib/qml/shader/qsb.js +27 -0
- package/lib/qml/shader/renderer.js +39 -0
- package/lib/qml/shader/runtime.js +61 -0
- package/lib/qml/shapes.js +251 -0
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +126 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/window-constraints.js +16 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +16 -1
- package/lib/renderer/canvas-2d.js +29 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +45 -17
- package/lib/renderer/main.js +21 -6
- package/lib/renderer/page-realm.js +20 -10
- package/lib/renderer/page-renderer.js +497 -42
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +10 -4
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/clipboard.test.js +19 -0
- package/test/computed-style.test.js +12 -0
- package/test/document-selection.test.js +27 -0
- package/test/double-click.test.js +85 -0
- package/test/fixtures/canvas-damage.js +40 -0
- package/test/fixtures/fake-renderer.js +1 -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/omastorm-helpers/Dockerfile +35 -0
- package/test/fixtures/omastorm-helpers/check.mjs +56 -0
- package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
- package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
- package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
- package/test/fixtures/pointer-paint-batching.js +23 -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-fonts/main.qml +10 -0
- package/test/fixtures/qml-locale/check.mjs +6 -0
- package/test/fixtures/qml-locale/main.qml +8 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/qml-shader/grab.qml +14 -0
- package/test/fixtures/qml-shader/gradient.frag +5 -0
- package/test/fixtures/qml-shader/main.qml +14 -0
- package/test/fixtures/qml-shader/texture-grab.qml +25 -0
- package/test/fixtures/qml-shader/texture-size.frag +5 -0
- package/test/fixtures/qml-shader-check.mjs +24 -0
- package/test/fixtures/title-no-paint.js +20 -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-double-click.html +21 -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-mouse-buttons.html +18 -0
- package/test/manual-omastorm-engine.mjs +19 -0
- package/test/manual-omastorm-shaders.mjs +28 -0
- package/test/manual-omastorm-software.mjs +25 -0
- package/test/manual-pointer-scroll.html +43 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-canvas.test.js +21 -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 +95 -0
- package/test/qml-controls-window.test.js +182 -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-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -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-locale.test.js +45 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-nonvisual-visible.test.js +19 -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-shader-runtime.test.js +6 -0
- package/test/qml-shortcuts.test.js +39 -0
- package/test/qml-socket.test.js +60 -0
- package/test/qml-software-shader.test.js +55 -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-window-update.test.js +36 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/render-interval.test.js +33 -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/title-no-paint.test.js +6 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +61 -0
|
@@ -22,10 +22,28 @@
|
|
|
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 software shader canvas bridge
|
|
33
|
+
import {paintQmlCanvas} from './canvas-2d.js';
|
|
34
|
+
import {createShaderRenderer} from '../qml/shader/renderer.js';
|
|
35
|
+
// qml handle end
|
|
36
|
+
|
|
37
|
+
// qml handle begin: optional document frontend
|
|
38
|
+
import {createQmlMediaController,applyQmlMediaGain} from '../qml/media-renderer.js';
|
|
39
|
+
import {INTERFACES as qmlBindings} from "./bindings.js";
|
|
40
|
+
import { enableQmlDomImages, hasQmlDomImages, captureQmlDomImage, filterQmlTextureSources, qmlTextureVisibilityChanged } from "../qml/canvas-dom.js";
|
|
41
|
+
import {loadQmlFonts} from '../qml/fonts.js';
|
|
42
|
+
import {createQmlHost} from "../qml/host.js";
|
|
43
|
+
import { loadQmlDocument } from "../qml/document.js";
|
|
44
|
+
import { isQmlDocument } from "../qml/format.js";
|
|
45
|
+
// qml handle end
|
|
46
|
+
|
|
29
47
|
import { parseHTMLDocument } from "../happy-dom/parser.js";
|
|
30
48
|
import { LayoutEngine } from "../layout/text-layout.js";
|
|
31
49
|
import { ScrollViewport } from "../layout/scroll-viewport.js";
|
|
@@ -227,6 +245,9 @@ function buildPageRenderer({
|
|
|
227
245
|
requestDownload = () => {},
|
|
228
246
|
reportError = () => {},
|
|
229
247
|
pageFetch = () => Promise.reject(new TypeError("network access is not available")),
|
|
248
|
+
clipboardReadInput = () => clipboardRead(),
|
|
249
|
+
clipboardWriteInput = text => clipboardWrite(text),
|
|
250
|
+
clipboardRead = () => Promise.reject(Error("Clipboard unavailable")),
|
|
230
251
|
clipboardWrite = () => Promise.reject(Error("Clipboard unavailable")),
|
|
231
252
|
webSockets = null,
|
|
232
253
|
audio = null,
|
|
@@ -242,6 +263,7 @@ function buildPageRenderer({
|
|
|
242
263
|
mobile = false,
|
|
243
264
|
verifyStyle = false,
|
|
244
265
|
fullStyle = false,
|
|
266
|
+
renderMinInterval = 16,
|
|
245
267
|
// An iframe's document (see syncFrames): frames go to the embedding page,
|
|
246
268
|
// which needs only the viewport, so nothing is painted ahead.
|
|
247
269
|
embedded = false,
|
|
@@ -252,6 +274,10 @@ function buildPageRenderer({
|
|
|
252
274
|
CanvasKit, loadedFamilies, fontCollection, fontFamilies, sansSerifFamily, monospaceFamily, monospaceBoldFamily,
|
|
253
275
|
genericFamilies, builtinTypefaces,
|
|
254
276
|
}) {
|
|
277
|
+
// qml handle begin: preserve ordinary page fonts across QML navigation
|
|
278
|
+
const standardFonts = {CanvasKit, loadedFamilies, fontCollection, fontFamilies, builtinTypefaces};
|
|
279
|
+
// qml handle end
|
|
280
|
+
|
|
255
281
|
let documentFonts = null;
|
|
256
282
|
let fontSheets = null;
|
|
257
283
|
let fontEnvironment = "";
|
|
@@ -315,6 +341,10 @@ function buildPageRenderer({
|
|
|
315
341
|
let framePaintTimer = null;
|
|
316
342
|
// The frame keyboard input goes to, after a click into it.
|
|
317
343
|
let focusedFrame = null;
|
|
344
|
+
let pointerButtons = 0, suppressPointerMouse = false;
|
|
345
|
+
const pointerPresses = new Map();
|
|
346
|
+
let selectionDrag = null;
|
|
347
|
+
let selectionVersion = 0;
|
|
318
348
|
const viewport = new ScrollViewport(800, 600);
|
|
319
349
|
const page = {
|
|
320
350
|
url: "about:blank",
|
|
@@ -505,7 +535,7 @@ function buildPageRenderer({
|
|
|
505
535
|
const paragraph = makeParagraph(text, computedStyle);
|
|
506
536
|
paragraph.layout(100_000);
|
|
507
537
|
// getLongestLine() drops trailing white space, which line breaking needs.
|
|
508
|
-
width = paragraph.getMaxIntrinsicWidth();
|
|
538
|
+
width = {width:paragraph.getMaxIntrinsicWidth(),ascent:paragraph.getAlphabeticBaseline(),height:paragraph.getHeight()};
|
|
509
539
|
paragraph.delete();
|
|
510
540
|
if (++measuredCount > MAX_MEASURED_TEXTS) {
|
|
511
541
|
measuredWidths.clear();
|
|
@@ -514,7 +544,7 @@ function buildPageRenderer({
|
|
|
514
544
|
}
|
|
515
545
|
widths.set(text, width);
|
|
516
546
|
}
|
|
517
|
-
return width;
|
|
547
|
+
return width.width;
|
|
518
548
|
}
|
|
519
549
|
|
|
520
550
|
// Computed styles are immutable and shared, so each one's key is computed once.
|
|
@@ -534,8 +564,12 @@ function buildPageRenderer({
|
|
|
534
564
|
}
|
|
535
565
|
|
|
536
566
|
const fontMetricsCache = new Map();
|
|
537
|
-
function fontMetrics(computedStyle = null) {
|
|
567
|
+
function fontMetrics(computedStyle = null, text = null) {
|
|
538
568
|
const key = fontKeyOf(computedStyle);
|
|
569
|
+
if(text) {
|
|
570
|
+
measureText(text,computedStyle);
|
|
571
|
+
return measuredWidths.get(key).get(text);
|
|
572
|
+
}
|
|
539
573
|
let metrics = fontMetricsCache.get(key);
|
|
540
574
|
if (!metrics) {
|
|
541
575
|
const paragraph = makeParagraph("Hg", computedStyle);
|
|
@@ -570,6 +604,7 @@ function buildPageRenderer({
|
|
|
570
604
|
let rasterSurface = null;
|
|
571
605
|
let rasterSurfaceWidth = 0;
|
|
572
606
|
let rasterSurfaceHeight = 0;
|
|
607
|
+
let rasterRegion = null;
|
|
573
608
|
const rasterReadback = new RasterReadback(CanvasKit, { shared: Boolean(onFrame), allocate: allocateFramePixels });
|
|
574
609
|
// A scroll or unrelated DOM update does not change text shaping. Keep a
|
|
575
610
|
// bounded set of visible paragraphs keyed by their complete shaping input.
|
|
@@ -637,7 +672,8 @@ function buildPageRenderer({
|
|
|
637
672
|
const hints = page.hintsVisible
|
|
638
673
|
? page.hints.map((hint) => `${hint.label}@${hint.x + hint.scrollX},${hint.y + hint.scrollY}`).join(";")
|
|
639
674
|
: "";
|
|
640
|
-
|
|
675
|
+
const selection=page.window?.getSelection();
|
|
676
|
+
return hints + selectionVersion + (selection?.rangeCount ? `:${selection.anchorOffset}:${selection.focusOffset}:${selection.toString()}` : "");
|
|
641
677
|
}
|
|
642
678
|
|
|
643
679
|
function sameRasterInput() {
|
|
@@ -682,6 +718,36 @@ function buildPageRenderer({
|
|
|
682
718
|
schedulePrepaint();
|
|
683
719
|
}
|
|
684
720
|
|
|
721
|
+
/** Conservative canvas-only damage, in document CSS pixels. */
|
|
722
|
+
function canvasDamage(region) {
|
|
723
|
+
const old = rasterized.list, next = page.displayList;
|
|
724
|
+
// Reuse pixels only when geometry and every non-canvas input are stable.
|
|
725
|
+
// Transformed/fixed canvases conservatively take the full-paint path.
|
|
726
|
+
if (!rasterSurface || !Bun.deepEquals(rasterRegion, region)
|
|
727
|
+
|| rasterized.scale !== page.deviceScaleFactor || rasterized.background !== page.backgroundColor
|
|
728
|
+
|| rasterized.overlay !== overlayKey() || rasterized.frames !== framesVersion
|
|
729
|
+
|| rasterized.scrollX !== viewport.x || rasterized.scrollY !== viewport.y
|
|
730
|
+
|| !old || !next || old.images.length !== next.images.length
|
|
731
|
+
|| !Bun.deepEquals(old.items, next.items)
|
|
732
|
+
|| next.items.some(item => item.op === 'drawVideo')) return null;
|
|
733
|
+
let damage = null;
|
|
734
|
+
for (let i = 0; i < next.images.length; i++) {
|
|
735
|
+
if (old.images[i] === next.images[i]) continue;
|
|
736
|
+
const uses = next.items.filter(item => item.image === i);
|
|
737
|
+
if (!uses.length) return null;
|
|
738
|
+
for (const item of uses) {
|
|
739
|
+
if (item.op !== 'drawImage' || item.fixed || item.transform
|
|
740
|
+
|| page.renderTree.nodesById.get(item.nodeId)?.domNode?.tagName !== 'CANVAS') return null;
|
|
741
|
+
const r = item.rect;
|
|
742
|
+
if (![r.x, r.y, r.width, r.height].every(Number.isFinite)) return null;
|
|
743
|
+
const left = r.x - 1, top = r.y - 1, right = r.x + r.width + 1, bottom = r.y + r.height + 1;
|
|
744
|
+
damage = damage ? {x:Math.min(damage.x,left),y:Math.min(damage.y,top),right:Math.max(damage.right,right),bottom:Math.max(damage.bottom,bottom)}
|
|
745
|
+
: {x:left,y:top,right,bottom};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return damage;
|
|
749
|
+
}
|
|
750
|
+
|
|
685
751
|
/** Rasterizes a page rectangle (CSS pixels) into RGBA pixels. */
|
|
686
752
|
function rasterTile(region) {
|
|
687
753
|
// Prepaint can run before a queued canvas/image repaint. Refresh native
|
|
@@ -693,6 +759,8 @@ function buildPageRenderer({
|
|
|
693
759
|
const scale = page.deviceScaleFactor;
|
|
694
760
|
const pixelWidth = Math.max(1, Math.round(region.width * scale));
|
|
695
761
|
const pixelHeight = Math.max(1, Math.round(region.height * scale));
|
|
762
|
+
const damage = canvasDamage(region);
|
|
763
|
+
if (damage) trace?.(`${page.url}: canvas damage ${damage.right-damage.x}x${damage.bottom-damage.y}`);
|
|
696
764
|
if (!rasterSurface || rasterSurfaceWidth !== pixelWidth || rasterSurfaceHeight !== pixelHeight) {
|
|
697
765
|
// MakeSurface owns a malloc-backed pixel buffer; delete() alone leaks it.
|
|
698
766
|
rasterSurface?.dispose();
|
|
@@ -703,9 +771,19 @@ function buildPageRenderer({
|
|
|
703
771
|
const surface = rasterSurface;
|
|
704
772
|
if (!surface) throw new Error(`Unable to create a ${pixelWidth}x${pixelHeight} CanvasKit surface`);
|
|
705
773
|
const canvas = surface.getCanvas();
|
|
774
|
+
canvas.save();
|
|
775
|
+
if (damage) canvas.clipRect(CanvasKit.LTRBRect(
|
|
776
|
+
Math.floor((damage.x-region.x)*scale), Math.floor((damage.y-region.y)*scale),
|
|
777
|
+
Math.ceil((damage.right-region.x)*scale), Math.ceil((damage.bottom-region.y)*scale),
|
|
778
|
+
), CanvasKit.ClipOp.Intersect, false);
|
|
779
|
+
// Clear and replay all layers inside the damage, including backgrounds
|
|
780
|
+
// and foreground occluders. Transparent canvas pixels must not leave trails.
|
|
706
781
|
canvas.clear(canvasColor(CanvasKit, page.backgroundColor));
|
|
707
|
-
paintDisplayList(canvas, page.displayList, region, scale
|
|
782
|
+
paintDisplayList(canvas, page.displayList, region, scale, null, damage ? {
|
|
783
|
+
x:damage.x, y:damage.y, width:damage.right-damage.x, height:damage.bottom-damage.y,
|
|
784
|
+
} : region);
|
|
708
785
|
drawHints(canvas, region);
|
|
786
|
+
canvas.restore();
|
|
709
787
|
surface.flush();
|
|
710
788
|
const image = surface.makeImageSnapshot();
|
|
711
789
|
let pixels;
|
|
@@ -713,6 +791,7 @@ function buildPageRenderer({
|
|
|
713
791
|
finally { image.delete(); }
|
|
714
792
|
const videoLayer = makeVideoLayer(canvas, surface, region, scale, pixelWidth, pixelHeight);
|
|
715
793
|
page.needsPaint = false;
|
|
794
|
+
rasterRegion = {...region};
|
|
716
795
|
rasterized = {
|
|
717
796
|
list: page.displayList, overlay: overlayKey(), background: page.backgroundColor, scale: page.deviceScaleFactor,
|
|
718
797
|
scrollX: viewport.x, scrollY: viewport.y, frames: framesVersion,
|
|
@@ -788,10 +867,10 @@ function buildPageRenderer({
|
|
|
788
867
|
frameTimer = hostSetTimeout(() => {
|
|
789
868
|
frameTimer = null;
|
|
790
869
|
refresh();
|
|
791
|
-
// Coalesce ordinary mutations for
|
|
792
|
-
// retain a longer idle window so related script mutations can settle
|
|
870
|
+
// Coalesce ordinary mutations for the configured minimum interval.
|
|
871
|
+
// Expensive style/layout rebuilds retain a longer idle window so related script mutations can settle
|
|
793
872
|
// instead of continuously rebuilding a large document.
|
|
794
|
-
}, Math.max(
|
|
873
|
+
}, Math.max(renderMinInterval, lastRebuildMs * 2));
|
|
795
874
|
}
|
|
796
875
|
|
|
797
876
|
/** The viewport as an encoded image (base64), cut from the latest tile. */
|
|
@@ -811,7 +890,7 @@ function buildPageRenderer({
|
|
|
811
890
|
/** Rasterizes display items intersecting the viewport onto a CanvasKit canvas. */
|
|
812
891
|
const paintFailures = new Set();
|
|
813
892
|
|
|
814
|
-
function paintDisplayList(canvas, displayList, view, scale, videoOverride = null) {
|
|
893
|
+
function paintDisplayList(canvas, displayList, view, scale, videoOverride = null, dirtyView = view) {
|
|
815
894
|
if (!displayList) return;
|
|
816
895
|
const pixel = (value) => Math.round(value * scale);
|
|
817
896
|
const deviceRect = (rect) => CanvasKit.XYWHRect(
|
|
@@ -838,7 +917,7 @@ function buildPageRenderer({
|
|
|
838
917
|
if (rounded) canvas.drawRRect(rounded, paint);
|
|
839
918
|
else canvas.drawRect(deviceRect(rect), paint);
|
|
840
919
|
};
|
|
841
|
-
for (const item of visibleItems(displayList,
|
|
920
|
+
for (const item of visibleItems(displayList, dirtyView, viewport)) {
|
|
842
921
|
const saved = item.fixed || item.clipRect || item.opacity < 1 || item.transform ? canvas.save() : null;
|
|
843
922
|
try {
|
|
844
923
|
if (item.fixed) canvas.translate(viewport.x * scale, viewport.y * scale);
|
|
@@ -1013,7 +1092,7 @@ function buildPageRenderer({
|
|
|
1013
1092
|
} else if (item.op === "drawSvg") {
|
|
1014
1093
|
drawSvg(item);
|
|
1015
1094
|
} else if (item.op === "drawText") {
|
|
1016
|
-
const entry = paragraphForPaint(item, scale, pixel(Math.max(1,
|
|
1095
|
+
const entry = paragraphForPaint(item, scale, pixel(Math.max(1, item.textWidth + 1)));
|
|
1017
1096
|
try {
|
|
1018
1097
|
if (item.gradient) {
|
|
1019
1098
|
// background-clip:text: draw glyphs, then keep the gradient only where they are.
|
|
@@ -1027,6 +1106,17 @@ function buildPageRenderer({
|
|
|
1027
1106
|
} else {
|
|
1028
1107
|
canvas.drawParagraph(entry.paragraph, pixel(item.x - view.x), pixel(item.y - view.y));
|
|
1029
1108
|
}
|
|
1109
|
+
const selected=selectedTextRange(item);
|
|
1110
|
+
if(selected){
|
|
1111
|
+
const boxes=entry.paragraph.getRectsForRange(selected.from,selected.to,CanvasKit.RectHeightStyle.Max,CanvasKit.RectWidthStyle.Tight);
|
|
1112
|
+
const white=paragraphForPaint({...item,color:'rgba(255, 255, 255, 1)',gradient:null},scale,pixel(Math.max(1,item.textWidth+1)));
|
|
1113
|
+
const paint=new CanvasKit.Paint();paint.setColor(canvasColor(CanvasKit,'rgba(56, 116, 242, 1)'));
|
|
1114
|
+
try{for(const box of boxes){
|
|
1115
|
+
const r=box.rect,ox=pixel(item.x-view.x),oy=pixel(item.y-view.y);
|
|
1116
|
+
const rect=CanvasKit.LTRBRect(r[0]+ox,r[1]+oy,r[2]+ox,r[3]+oy);
|
|
1117
|
+
canvas.save();canvas.clipRect(rect,CanvasKit.ClipOp.Intersect,false);canvas.drawRect(rect,paint);canvas.drawParagraph(white.paragraph,ox,oy);canvas.restore();
|
|
1118
|
+
}}finally{paint.delete();if(!white.retained)white.paragraph.delete();}
|
|
1119
|
+
}
|
|
1030
1120
|
} finally {
|
|
1031
1121
|
if (!entry.retained) entry.paragraph.delete();
|
|
1032
1122
|
}
|
|
@@ -1383,6 +1473,14 @@ function buildPageRenderer({
|
|
|
1383
1473
|
const traits = selectorTraitsFor(sheets);
|
|
1384
1474
|
const records = styleChanges.records;
|
|
1385
1475
|
if (styleChanges.observer) records.push(...styleChanges.observer.takeRecords());
|
|
1476
|
+
|
|
1477
|
+
// qml handle begin: QML visual attributes invalidate retained rendering
|
|
1478
|
+
if (hasQmlDomImages(page.document) && qmlTextureVisibilityChanged(records)) {
|
|
1479
|
+
page.resourcePaintDirty = true;
|
|
1480
|
+
page.needsPaint = true;
|
|
1481
|
+
}
|
|
1482
|
+
// qml handle end
|
|
1483
|
+
|
|
1386
1484
|
const roots = styleChanges.roots;
|
|
1387
1485
|
// Explicit roots mean that state-dependent selectors may need restyling;
|
|
1388
1486
|
// they do not by themselves change render-tree content. DOM mutation
|
|
@@ -1549,18 +1647,23 @@ function buildPageRenderer({
|
|
|
1549
1647
|
|
|
1550
1648
|
function replaceParsedDocument(parsed, styleSheets = [], imageResources = emptyImageResources(), linkSheets = null) {
|
|
1551
1649
|
if (page.document) loadImageResource.cancelDocument(page.document);
|
|
1650
|
+
resetInputCaret();
|
|
1552
1651
|
rasterReadback.dispose();
|
|
1553
1652
|
clearPaintParagraphs();
|
|
1554
1653
|
resetDocumentFonts();
|
|
1555
1654
|
disposeFrames();
|
|
1556
1655
|
closePiano();
|
|
1557
1656
|
disposeVideos();
|
|
1657
|
+
// qml handle begin: dispose application resources with their document
|
|
1658
|
+
page.document?.__qmlHost?.dispose();
|
|
1659
|
+
// qml handle end
|
|
1558
1660
|
page.realm?.dispose();
|
|
1559
1661
|
page.realm = null;
|
|
1560
1662
|
page.window?.happyDOM?.close?.();
|
|
1561
1663
|
for (const image of page.decodedImages) image.delete();
|
|
1562
1664
|
disposeBackgrounds();
|
|
1563
1665
|
page.window = parsed.window;
|
|
1666
|
+
pointerButtons = 0; suppressPointerMouse = false; pointerPresses.clear(); selectionDrag=null;
|
|
1564
1667
|
page.document = parsed.document;
|
|
1565
1668
|
observeDocumentCanvases(parsed.document, (node) => {
|
|
1566
1669
|
if (disposedRenderer || node.ownerDocument !== page.document || !node.isConnected) return;
|
|
@@ -1762,6 +1865,16 @@ function buildPageRenderer({
|
|
|
1762
1865
|
onLifecycle?.("DOMContentLoaded", generation);
|
|
1763
1866
|
onLifecycle?.("load", generation);
|
|
1764
1867
|
};
|
|
1868
|
+
// qml handle begin: optional local Qt font directory, never inherited by HTML
|
|
1869
|
+
const selectedFonts = page.scripting === "host" && url.startsWith("file:") && isQmlDocument(url, contentType)
|
|
1870
|
+
? await loadQmlFonts(standardFonts, message => reportError(new Error(message))) : standardFonts;
|
|
1871
|
+
if (!current()) return;
|
|
1872
|
+
if (fontCollection !== selectedFonts.fontCollection) {
|
|
1873
|
+
({loadedFamilies, fontCollection, fontFamilies, builtinTypefaces} = selectedFonts);
|
|
1874
|
+
clearFontCaches();
|
|
1875
|
+
}
|
|
1876
|
+
// qml handle end
|
|
1877
|
+
|
|
1765
1878
|
page.url = url;
|
|
1766
1879
|
page.cookie = typeof cookie === "string" ? cookie : "";
|
|
1767
1880
|
if (error) {
|
|
@@ -1797,8 +1910,23 @@ function buildPageRenderer({
|
|
|
1797
1910
|
trace(`${url}: ${name} ${Math.round(now - phaseStart)}ms`);
|
|
1798
1911
|
phaseStart = now;
|
|
1799
1912
|
} : () => {};
|
|
1800
|
-
|
|
1801
|
-
|
|
1913
|
+
|
|
1914
|
+
let isHtml = contentType.toLowerCase().includes("html");
|
|
1915
|
+
let parsed;
|
|
1916
|
+
|
|
1917
|
+
// qml handle begin: supply DOM before the default HTML parser
|
|
1918
|
+
if (isQmlDocument(url, contentType)) {
|
|
1919
|
+
const qmlHost = page.scripting === "host" && new URL(url).protocol === "file:" ? createQmlHost(url) : null;
|
|
1920
|
+
parsed = await loadQmlDocument(source, url, {scripting: page.scripting !== "off", fetchResource, importRoots:qmlHost?.importRoots});
|
|
1921
|
+
parsed.document.__qmlHost = qmlHost;
|
|
1922
|
+
if (qmlHost) enableQmlDomImages(parsed.document);
|
|
1923
|
+
if (!current()) {qmlHost?.dispose();return;}
|
|
1924
|
+
// Reuse the existing stylesheet/script/lifecycle pipeline for QML DOM.
|
|
1925
|
+
isHtml = true;
|
|
1926
|
+
}
|
|
1927
|
+
// qml handle end
|
|
1928
|
+
|
|
1929
|
+
parsed ??= parseHTMLDocument(isHtml ? source : "<body></body>", url, referrer);
|
|
1802
1930
|
phase("parse");
|
|
1803
1931
|
// Stylesheets block rendering and scripts; images do not (only the
|
|
1804
1932
|
// window load event waits for them, see whenImagesLoaded): one slow image
|
|
@@ -1858,6 +1986,43 @@ function buildPageRenderer({
|
|
|
1858
1986
|
mode: page.scripting,
|
|
1859
1987
|
hooks: realmHooks(generation),
|
|
1860
1988
|
});
|
|
1989
|
+
// qml handle begin: only a local QML host realm receives this capability
|
|
1990
|
+
if (page.document.__qmlHost) page.realm.pageGlobal.__qmlHost = page.document.__qmlHost;
|
|
1991
|
+
// qml handle end
|
|
1992
|
+
|
|
1993
|
+
// qml handle begin: QML methods/properties adapt shared media and text controls
|
|
1994
|
+
if(page.document.__qmlHost) {
|
|
1995
|
+
const host=page.document.__qmlHost;
|
|
1996
|
+
host.media=element=>{
|
|
1997
|
+
element=qmlBindings.Node.unwrap(element)??element;
|
|
1998
|
+
if(element?.ownerDocument!==page.document||!['VIDEO','AUDIO'].includes(element.tagName))throw Error('Invalid QML media element');
|
|
1999
|
+
return createQmlMediaController(element,{
|
|
2000
|
+
valid:e=>e.ownerDocument===page.document,state:e=>page.media.get(e),
|
|
2001
|
+
close:closeMedia,forget:e=>{page.media.delete(e);page.resources.delete(e);},ensure:mediaStateFor,
|
|
2002
|
+
load:loadMedia,seek:seekMedia,play:playMedia,pause:pauseMedia,
|
|
2003
|
+
snapshot:mediaState,error:mediaError,gains:qmlMediaGains,
|
|
2004
|
+
});
|
|
2005
|
+
};
|
|
2006
|
+
|
|
2007
|
+
host.inputClipboard=(element,operation)=>clipboardForInput(qmlBindings.Node.unwrap(element)??element,operation,{read:()=>host.clipboardRead(),write:text=>host.clipboardWrite(text)});
|
|
2008
|
+
host.inputPresentation=(element,options)=>{
|
|
2009
|
+
element=qmlBindings.Node.unwrap(element)??element;
|
|
2010
|
+
if(element?.ownerDocument!==page.document)return;
|
|
2011
|
+
inputPresentation.set(element,Object.assign(inputPresentation.get(element)||{},options));invalidateInput();
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
// qml handle end
|
|
2015
|
+
|
|
2016
|
+
// qml handle begin: local QML fragment shader capability
|
|
2017
|
+
if (page.document.__qmlHost) {
|
|
2018
|
+
const host=page.document.__qmlHost;
|
|
2019
|
+
host.shader=createShaderRenderer({kit:CanvasKit,unwrap:e=>qmlBindings.Node.unwrap(e),
|
|
2020
|
+
valid:e=>e?.ownerDocument===page.document,
|
|
2021
|
+
resource:e=>e.tagName==='CANVAS'?canvasResource(e):page.resources.get(e),
|
|
2022
|
+
paint:paintQmlCanvas,track:fn=>host.trackResource(fn)});
|
|
2023
|
+
}
|
|
2024
|
+
// qml handle end
|
|
2025
|
+
|
|
1861
2026
|
await page.realm.runDocumentScripts();
|
|
1862
2027
|
if (!current()) return;
|
|
1863
2028
|
phase("scripts");
|
|
@@ -1957,7 +2122,9 @@ function buildPageRenderer({
|
|
|
1957
2122
|
/** Brings style, layout and raster up to date; returns whether it repainted. */
|
|
1958
2123
|
function refresh() {
|
|
1959
2124
|
if (disposedRenderer) return false;
|
|
2125
|
+
syncInputCaret();
|
|
1960
2126
|
if (page.dirty) rebuildRenderTree();
|
|
2127
|
+
if(followCaret) { followCaret=false; revealInputCaret(); }
|
|
1961
2128
|
if (!page.needsPaint) return false;
|
|
1962
2129
|
render();
|
|
1963
2130
|
return true;
|
|
@@ -1978,7 +2145,159 @@ function buildPageRenderer({
|
|
|
1978
2145
|
if (!prevented && element.tagName === "DETAILS") element.open = !element.open;
|
|
1979
2146
|
}
|
|
1980
2147
|
|
|
1981
|
-
|
|
2148
|
+
// qml handle begin: optional per-player PCM gain
|
|
2149
|
+
const qmlMediaGains=new WeakMap();
|
|
2150
|
+
// qml handle end
|
|
2151
|
+
|
|
2152
|
+
let caretElement, caretKey, caretTimer, followCaret=false;
|
|
2153
|
+
function resetInputCaret() {
|
|
2154
|
+
hostClearInterval(caretTimer);caretTimer=null;caretElement=null;caretKey=null;
|
|
2155
|
+
}
|
|
2156
|
+
function invalidateInput() {
|
|
2157
|
+
page.resourceLayoutDirty=true;page.dirty=true;page.needsPaint=true;scheduleFrame();
|
|
2158
|
+
}
|
|
2159
|
+
function syncInputCaret() {
|
|
2160
|
+
const element=page.document?.activeElement;
|
|
2161
|
+
const valid=isTextInput(element)&&element.isConnected;
|
|
2162
|
+
const key=valid?`${element.selectionStart}:${element.selectionEnd}:${element.value}:${inputPresentation.get(element)?.visible}`:'';
|
|
2163
|
+
if(element===caretElement&&key===caretKey)return;
|
|
2164
|
+
resetInputCaret();caretElement=element;caretKey=key;followCaret=true;
|
|
2165
|
+
if(!valid)return;
|
|
2166
|
+
const options=inputPresentation.get(element)||{};options.on=true;inputPresentation.set(element,options);
|
|
2167
|
+
page.resourceLayoutDirty=true;page.dirty=true;page.needsPaint=true;
|
|
2168
|
+
if(element.selectionStart!==element.selectionEnd||options.visible===false)return;
|
|
2169
|
+
caretTimer=hostSetInterval(()=>{
|
|
2170
|
+
if(page.document?.activeElement!==element||!element.isConnected){resetInputCaret();invalidateInput();return;}
|
|
2171
|
+
options.on=!options.on;invalidateInput();
|
|
2172
|
+
},500);
|
|
2173
|
+
caretTimer.unref?.();
|
|
2174
|
+
}
|
|
2175
|
+
function revealInputCaret() {
|
|
2176
|
+
const element=page.document?.activeElement;
|
|
2177
|
+
if(!isTextInput(element))return;
|
|
2178
|
+
const position=element.selectionDirection==='backward'?element.selectionStart:element.selectionEnd;
|
|
2179
|
+
if(position===0){setElementScroll(element,0,0);return;}
|
|
2180
|
+
const caret=layoutBoxOf(inputCaretNode(element)),field=layoutBoxOf(element),geometry=scrollerGeometry(element);
|
|
2181
|
+
if(!caret||!field||!geometry)return;
|
|
2182
|
+
const [top,, ,left]=field.node.style.borderWidths||[0,0,0,0];
|
|
2183
|
+
const padding=field.node.style.padding||[0,0,0,0];
|
|
2184
|
+
const x=caret.box.x-field.box.x-left,y=caret.box.y-field.box.y-top;
|
|
2185
|
+
const px=Number(padding[3])||0,py=Number(padding[0])||0;
|
|
2186
|
+
let sx=geometry.x,sy=geometry.y;
|
|
2187
|
+
if(x<sx+px)sx=x-px;
|
|
2188
|
+
else if(x+1>sx+geometry.clientWidth-px)sx=x+1-geometry.clientWidth+px;
|
|
2189
|
+
if(y<sy+py)sy=y-py;
|
|
2190
|
+
else if(y+caret.box.height>sy+geometry.clientHeight-py)sy=y+caret.box.height-geometry.clientHeight+py;
|
|
2191
|
+
setElementScroll(element,sx,sy);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// Hit the same laid-out/shaped runs that paint the control. This is only
|
|
2195
|
+
// performed on a pointer press, never on the animation/capture path.
|
|
2196
|
+
function inputPositionAtPoint(element,x,y) {
|
|
2197
|
+
if(!String(element.value??''))return 0;
|
|
2198
|
+
let best=null;
|
|
2199
|
+
for(const hit of page.displayList?.hits??[]) {
|
|
2200
|
+
const run=hit.textRun;if(!run)continue;
|
|
2201
|
+
const node=page.renderTree.nodesById.get(hit.nodeId);
|
|
2202
|
+
if((inputOwner.get(node?.domNode)??node?.domNode)!==element)continue;
|
|
2203
|
+
let px=x+(hit.fixed?0:viewport.x),py=y+(hit.fixed?0:viewport.y);
|
|
2204
|
+
if(hit.transform){
|
|
2205
|
+
const {a,d,e,f}=hit.transform;if(!a||!d)continue;
|
|
2206
|
+
px=(px-e)/a;py=(py-f)/d;
|
|
2207
|
+
}
|
|
2208
|
+
const top=run.y, height=parseFloat(run.style.lineHeight)||parseFloat(run.style.fontSize)||16;
|
|
2209
|
+
const dy=Math.max(top-py,py-top-height,0),dx=Math.max(run.x-px,px-run.x-run.width,0);
|
|
2210
|
+
if(!best||dy<best.dy||dy===best.dy&&dx<best.dx)best={run,node,px,dy,dx};
|
|
2211
|
+
}
|
|
2212
|
+
if(!best)return 0;
|
|
2213
|
+
const {run,node,px}=best;
|
|
2214
|
+
const paragraph=makeParagraph(run.text,run.style);let at;
|
|
2215
|
+
try{
|
|
2216
|
+
paragraph.layout(Math.max(1,run.width+1));
|
|
2217
|
+
at=paragraph.getGlyphPositionAtCoordinate(px-run.x,0).pos;
|
|
2218
|
+
}finally{paragraph.delete();}
|
|
2219
|
+
let position=(node.valueStart??0)+(run.sourceStart??0)+at;
|
|
2220
|
+
if(element.type==='password')position=[...String(element.value)].slice(0,position).join('').length;
|
|
2221
|
+
return Math.max(0,Math.min(String(element.value).length,position));
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function selectableText(node){
|
|
2225
|
+
for(let el=node.parentElement;el;el=el.parentElement){
|
|
2226
|
+
const value=styleEngine.get(el)?.userSelect;
|
|
2227
|
+
if(value==='none')return false;
|
|
2228
|
+
if(value==='text'||value==='all')return true;
|
|
2229
|
+
}
|
|
2230
|
+
return true;
|
|
2231
|
+
}
|
|
2232
|
+
function documentTextPoint(x,y,target=null) {
|
|
2233
|
+
let best=null;
|
|
2234
|
+
for(const hit of page.displayList?.hits??[]){
|
|
2235
|
+
const run=hit.textRun,node=page.renderTree.nodesById.get(hit.nodeId)?.domNode;
|
|
2236
|
+
if(!run||node?.nodeType!==3||!node.isConnected||!selectableText(node))continue;
|
|
2237
|
+
if(target && !target.contains(node))continue;
|
|
2238
|
+
if(node.parentElement?.closest('button,input,textarea,select,canvas,script,style'))continue;
|
|
2239
|
+
let px=x+(hit.fixed?0:viewport.x),py=y+(hit.fixed?0:viewport.y);
|
|
2240
|
+
if(hit.transform){const {a,d,e,f}=hit.transform;if(!a||!d)continue;px=(px-e)/a;py=(py-f)/d;}
|
|
2241
|
+
if(hit.clipRect && (px<hit.clipRect.x||py<hit.clipRect.y||px>hit.clipRect.x+hit.clipRect.width||py>hit.clipRect.y+hit.clipRect.height))continue;
|
|
2242
|
+
const height=parseFloat(run.style.lineHeight)||parseFloat(run.style.fontSize)||16;
|
|
2243
|
+
const dy=Math.max(run.y-py,py-run.y-height,0),dx=Math.max(run.x-px,px-run.x-run.width,0);
|
|
2244
|
+
if(!best||dy<best.dy||dy===best.dy&&dx<best.dx)best={run,node,px,dy,dx};
|
|
2245
|
+
}
|
|
2246
|
+
if(!best)return null;
|
|
2247
|
+
const {run,node,px}=best,p=makeParagraph(run.text,run.style);
|
|
2248
|
+
try{p.layout(Math.max(1,run.width+1));return {node,offset:Math.max(0,Math.min(node.textContent.length,(run.sourceStart??0)+p.getGlyphPositionAtCoordinate(px-run.x,0).pos))};}
|
|
2249
|
+
finally{p.delete();}
|
|
2250
|
+
}
|
|
2251
|
+
function extendDrag(x,y){
|
|
2252
|
+
if(selectionDrag.element){
|
|
2253
|
+
const el=selectionDrag.element;if(!el.isConnected||page.document.activeElement!==el){selectionDrag=null;return;}
|
|
2254
|
+
selectInput(el,selectionDrag.anchor,inputPositionAtPoint(el,x,y));syncInputCaret();invalidateInput();
|
|
2255
|
+
}else{
|
|
2256
|
+
const point=documentTextPoint(x,y);if(!point||!selectionDrag.node.isConnected)return;
|
|
2257
|
+
page.window.getSelection().setBaseAndExtent(selectionDrag.node,selectionDrag.offset,point.node,point.offset);
|
|
2258
|
+
selectionVersion++;
|
|
2259
|
+
page.needsPaint=true;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
function selectedTextRange(item){
|
|
2263
|
+
const sel=page.window.getSelection(),node=page.renderTree.nodesById.get(item.nodeId)?.domNode;
|
|
2264
|
+
if(!sel?.rangeCount||sel.isCollapsed||node?.nodeType!==3||!node.isConnected||!selectableText(node))return null;
|
|
2265
|
+
const range=sel.getRangeAt(0);if(!range.intersectsNode(node))return null;
|
|
2266
|
+
const base=item.sourceStart??0;
|
|
2267
|
+
const from=Math.max(0,(range.startContainer===node?range.startOffset:0)-base);
|
|
2268
|
+
const to=Math.min(item.text.length,(range.endContainer===node?range.endOffset:node.textContent.length)-base);
|
|
2269
|
+
return to>from?{from,to}:null;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
function clipboardForInput(element,operation,backend={read:clipboardReadInput,write:clipboardWriteInput}) {
|
|
2273
|
+
return inputClipboard(element,operation,{...backend,replace:(text,type)=>{
|
|
2274
|
+
if(page.document.activeElement!==element)return;
|
|
2275
|
+
editFocusedText(text,false,type);syncInputCaret();invalidateInput();
|
|
2276
|
+
}});
|
|
2277
|
+
}
|
|
2278
|
+
function inputDefault(element,event) {
|
|
2279
|
+
if(!isTextInput(element)||event.altKey)return false;
|
|
2280
|
+
if(moveInputSelection(element,event)){syncInputCaret();invalidateInput();return true;}
|
|
2281
|
+
if((event.ctrlKey||event.metaKey)&&['c','x','v'].includes(event.key.toLowerCase())) {
|
|
2282
|
+
let backend;
|
|
2283
|
+
|
|
2284
|
+
// qml handle begin: local applications choose their frontend clipboard transport
|
|
2285
|
+
const host=page.document.__qmlHost;
|
|
2286
|
+
if(host)backend={read:()=>host.clipboardRead(),write:text=>host.clipboardWrite(text)};
|
|
2287
|
+
// qml handle end
|
|
2288
|
+
|
|
2289
|
+
clipboardForInput(element,{c:'copy',x:'cut',v:'paste'}[event.key.toLowerCase()],backend).catch(reportError);
|
|
2290
|
+
return true;
|
|
2291
|
+
}
|
|
2292
|
+
if(event.key==='Delete'&&!element.readOnly) {
|
|
2293
|
+
const at=element.selectionStart;
|
|
2294
|
+
if(at===element.selectionEnd)selectInput(element,at,at+(String(element.value).codePointAt(at)>0xffff?2:1));
|
|
2295
|
+
editFocusedText('',false,'deleteContentForward');invalidateInput();return true;
|
|
2296
|
+
}
|
|
2297
|
+
return false;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
function editFocusedText(text, backspace = false, inputType = 'insertText') {
|
|
1982
2301
|
const element = page.document.activeElement;
|
|
1983
2302
|
if (!element || !["INPUT", "TEXTAREA"].includes(element.tagName) || element.disabled || element.readOnly) return;
|
|
1984
2303
|
if (element.tagName === "INPUT" && !["text", "search", "url", "email", "tel", "password"].includes(element.type)) return;
|
|
@@ -1993,14 +2312,14 @@ function buildPageRenderer({
|
|
|
1993
2312
|
if (page.realm) {
|
|
1994
2313
|
const prevented = page.realm.dispatch(element, new page.window.InputEvent("beforeinput", {
|
|
1995
2314
|
bubbles: true, cancelable: true, data: backspace ? null : text,
|
|
1996
|
-
inputType: backspace ? "deleteContentBackward" :
|
|
2315
|
+
inputType: backspace ? "deleteContentBackward" : inputType,
|
|
1997
2316
|
}));
|
|
1998
2317
|
if (prevented) return;
|
|
1999
2318
|
}
|
|
2000
2319
|
element.value = value.slice(0, start) + text + value.slice(end);
|
|
2001
2320
|
markStyleDirty(element); // :placeholder-shown, :valid / :invalid
|
|
2002
2321
|
element.setSelectionRange?.(start + text.length, start + text.length);
|
|
2003
|
-
if (page.realm) page.realm.dispatch(element, new page.window.
|
|
2322
|
+
if (page.realm) page.realm.dispatch(element, new page.window.InputEvent("input", { bubbles: true, inputType:backspace?"deleteContentBackward":inputType, data:backspace?null:text }));
|
|
2004
2323
|
page.dirty = true;
|
|
2005
2324
|
}
|
|
2006
2325
|
|
|
@@ -2064,6 +2383,20 @@ function buildPageRenderer({
|
|
|
2064
2383
|
canvasImage(element) {
|
|
2065
2384
|
if (!element) return null;
|
|
2066
2385
|
if (element.tagName === "CANVAS") return canvasResource(element);
|
|
2386
|
+
|
|
2387
|
+
// qml handle begin: local QML may draw a DOM subtree into a canvas
|
|
2388
|
+
if (hasQmlDomImages(page.document) && !["IMG", "VIDEO", "AUDIO", "IMAGE"].includes(element.tagName)) {
|
|
2389
|
+
if (page.dirty) rebuildRenderTree();
|
|
2390
|
+
return captureQmlDomImage(element, {
|
|
2391
|
+
document: page.document, tree: page.renderTree, layout: page.layout,
|
|
2392
|
+
resources: page.resources, backgroundResource, scrollOffsetOf,
|
|
2393
|
+
kit: CanvasKit, paint: paintDisplayList, canvasResource,
|
|
2394
|
+
isOriginClean: resource => resource?.originClean ?? Boolean(resource?.url &&
|
|
2395
|
+
(resource.url.startsWith("data:") || originOf(resource.url) === originOf(page.url))),
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
// qml handle end
|
|
2399
|
+
|
|
2067
2400
|
const resource = page.resources.get(element);
|
|
2068
2401
|
if (!resource?.image) return null;
|
|
2069
2402
|
const url = resource.url ?? element.src ?? "";
|
|
@@ -2105,6 +2438,7 @@ function buildPageRenderer({
|
|
|
2105
2438
|
}
|
|
2106
2439
|
},
|
|
2107
2440
|
whenImagesLoaded: () => Promise.all([page.imagesLoaded, documentFonts.ready(), ...[...page.backgrounds.values()].map((entry) => entry.loaded)]),
|
|
2441
|
+
selectionChanged(){selectionVersion++;page.needsPaint=true;scheduleFrame();},
|
|
2108
2442
|
afterTask() {},
|
|
2109
2443
|
boundsOf(element) {
|
|
2110
2444
|
if (page.dirty) rebuildRenderTree("script read geometry");
|
|
@@ -2196,6 +2530,7 @@ function buildPageRenderer({
|
|
|
2196
2530
|
page.cookie = mergeDocumentCookie(page.cookie, String(value));
|
|
2197
2531
|
setCookie(String(value));
|
|
2198
2532
|
},
|
|
2533
|
+
clipboardRead,
|
|
2199
2534
|
clipboardWrite,
|
|
2200
2535
|
webSockets,
|
|
2201
2536
|
pageFetch(request) {
|
|
@@ -2533,9 +2868,9 @@ function buildPageRenderer({
|
|
|
2533
2868
|
schedulePrepaint();
|
|
2534
2869
|
},
|
|
2535
2870
|
title() {
|
|
2536
|
-
|
|
2537
|
-
//
|
|
2538
|
-
return page.document?.title
|
|
2871
|
+
// DOM title changes are synchronous and do not require layout or paint.
|
|
2872
|
+
// An empty document title is also a live value, not a cached-title fallback.
|
|
2873
|
+
return page.document?.title ?? page.title;
|
|
2539
2874
|
},
|
|
2540
2875
|
scroll(deltaX, deltaY) {
|
|
2541
2876
|
if (viewport.scrollBy(deltaX, deltaY)) render();
|
|
@@ -2560,7 +2895,11 @@ function buildPageRenderer({
|
|
|
2560
2895
|
};
|
|
2561
2896
|
const prevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keydown", init));
|
|
2562
2897
|
let activated = false;
|
|
2563
|
-
|
|
2898
|
+
const inputHandled=!prevented&&inputDefault(target,init);
|
|
2899
|
+
if(!prevented&&!inputHandled&&(init.ctrlKey||init.metaKey)&&init.key.toLowerCase()==='c'){
|
|
2900
|
+
const text=page.window.getSelection().toString();if(text)clipboardWriteInput(text).catch(reportError);
|
|
2901
|
+
}
|
|
2902
|
+
if (!inputHandled && !prevented && !init.ctrlKey && !init.metaKey && !init.altKey) {
|
|
2564
2903
|
if ((key === "Enter" || key === " ") && isDisclosureControl(target)) {
|
|
2565
2904
|
activated = true;
|
|
2566
2905
|
if (key === "Enter") activateDisclosure(target);
|
|
@@ -2579,7 +2918,7 @@ function buildPageRenderer({
|
|
|
2579
2918
|
}
|
|
2580
2919
|
const upPrevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keyup", init));
|
|
2581
2920
|
if (activated && key === " " && !upPrevented) activateDisclosure(target);
|
|
2582
|
-
if (refresh() || prevented || activated) return { x: viewport.x, y: viewport.y };
|
|
2921
|
+
if (refresh() || prevented || activated || inputHandled) return { x: viewport.x, y: viewport.y };
|
|
2583
2922
|
}
|
|
2584
2923
|
const pageStep = Math.max(31, viewport.height - 31);
|
|
2585
2924
|
const delta = {
|
|
@@ -2629,18 +2968,29 @@ function buildPageRenderer({
|
|
|
2629
2968
|
return wheel(Number(x), Number(y), Number(deltaX) || 0, Number(deltaY) || 0, options);
|
|
2630
2969
|
},
|
|
2631
2970
|
/** Move the pointer in viewport coordinates, including boundary events. */
|
|
2632
|
-
move(x, y) {
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2971
|
+
move(x, y, options = {}) {
|
|
2972
|
+
// Hit testing needs current geometry, not a rasterized frame. Dispatch
|
|
2973
|
+
// every motion (drawing needs every point), but let the frame scheduler
|
|
2974
|
+
// combine their paints instead of rasterizing the whole page per event.
|
|
2975
|
+
if (page.dirty) rebuildRenderTree("pointer hit test");
|
|
2976
|
+
const target = elementAtPoint(page.renderTree, page.displayList, Number(x), Number(y), viewport) ?? (pointerButtons ? page.document.documentElement : null);
|
|
2977
|
+
const inside = target?.tagName === 'IFRAME' ? frameAt(target, x, y) : null;
|
|
2978
|
+
if (inside) inside.frame.renderer.move(inside.x, inside.y, options);
|
|
2979
|
+
dispatchPointerMove(page.window, page.realm, target, Number(x), Number(y), {...options, buttons: pointerButtons, suppressMouse:suppressPointerMouse});
|
|
2980
|
+
if(selectionDrag && (pointerButtons&1)) extendDrag(x,y);
|
|
2981
|
+
if (page.dirty || page.needsPaint) scheduleFrame();
|
|
2637
2982
|
},
|
|
2638
2983
|
/**
|
|
2639
2984
|
* Clicks at a viewport point: the page receives a click event first, and
|
|
2640
2985
|
* unless it calls preventDefault() a link under the point is returned for
|
|
2641
2986
|
* the controller to navigate to.
|
|
2642
2987
|
*/
|
|
2643
|
-
|
|
2988
|
+
pointerButton(x, y, button, state, options = {}) {
|
|
2989
|
+
if (button < 272 || button > 274) return null;
|
|
2990
|
+
return this.click(x, y, {...options, button: button === 273 ? 'right' : button === 274 ? 'middle' : 'left', phase: state ? 'down' : 'up'});
|
|
2991
|
+
},
|
|
2992
|
+
click(x, y, options = {}) {
|
|
2993
|
+
const detail = Number.isInteger(options.clickCount) && options.clickCount > 0 ? options.clickCount : 1;
|
|
2644
2994
|
refresh();
|
|
2645
2995
|
let region = hitTest(page.renderTree, page.layout, x, y, viewport, viewport);
|
|
2646
2996
|
let forwarded = false;
|
|
@@ -2666,7 +3016,7 @@ function buildPageRenderer({
|
|
|
2666
3016
|
// and a search form's forwarded field takes the click on the form.
|
|
2667
3017
|
const hitNode = page.displayList ? renderNodeAtPoint(page.renderTree, page.displayList, x, y, viewport) : null;
|
|
2668
3018
|
const precise = hitNode?.domNode?.nodeType === 1 ? hitNode.domNode : hitNode?.domNode?.parentElement;
|
|
2669
|
-
const target = forwarded ? region.element : precise ?? region?.element ?? null;
|
|
3019
|
+
const target = options.activationTarget ?? (forwarded ? region.element : inputOwner.get(precise) ?? precise ?? region?.element ?? (options.phase ? page.document.documentElement : null));
|
|
2670
3020
|
const generatedDisclosure = hitNode?.pseudo === "summary" && target?.tagName === "DETAILS";
|
|
2671
3021
|
if (!target) {
|
|
2672
3022
|
focusedFrame = null;
|
|
@@ -2679,11 +3029,97 @@ function buildPageRenderer({
|
|
|
2679
3029
|
const inFrame = target.tagName === "IFRAME" ? frameAt(target, x, y) : null;
|
|
2680
3030
|
focusedFrame = inFrame?.frame ?? null;
|
|
2681
3031
|
if (inFrame) {
|
|
2682
|
-
const followed = inFrame.frame.renderer.click(inFrame.x, inFrame.y);
|
|
3032
|
+
const followed = inFrame.frame.renderer.click(inFrame.x, inFrame.y, options);
|
|
2683
3033
|
const href = typeof followed === "string" ? followed : followed?.fragmentFrom ? null : followed?.url;
|
|
2684
3034
|
if (href && !followed?.download) void loadFrame(inFrame.frame, href);
|
|
2685
3035
|
return null;
|
|
2686
3036
|
}
|
|
3037
|
+
const mouseButton = options.button === 'right' || options.button === 2 ? 2
|
|
3038
|
+
: options.button === 'middle' || options.button === 1 ? 1 : 0;
|
|
3039
|
+
// The press focuses the focusable element it lands in, unless mousedown was canceled.
|
|
3040
|
+
const focus = () => {
|
|
3041
|
+
selectionDrag=null;
|
|
3042
|
+
const focusable = generatedDisclosure ? target : target.closest?.("input, textarea, select, button, summary, [tabindex]");
|
|
3043
|
+
if (focusable && !focusable.disabled) {
|
|
3044
|
+
const position=isTextInput(focusable)&&!forwarded?inputPositionAtPoint(focusable,x,y):null;
|
|
3045
|
+
focusable.focus();
|
|
3046
|
+
if(position!==null){selectInput(focusable,position);syncInputCaret();invalidateInput();if(options.phase==='down')selectionDrag={element:focusable,anchor:position};}
|
|
3047
|
+
} else if(mouseButton===0){
|
|
3048
|
+
const point=documentTextPoint(x,y,target);
|
|
3049
|
+
if(point)page.document.activeElement?.blur?.();
|
|
3050
|
+
selectionVersion++;page.window.getSelection().removeAllRanges();
|
|
3051
|
+
if(point){page.window.getSelection().collapse(point.node,point.offset);if(options.phase==='down')selectionDrag=point;}
|
|
3052
|
+
page.needsPaint=true;
|
|
3053
|
+
}
|
|
3054
|
+
};
|
|
3055
|
+
const modifiers = options.modifiers ?? 0;
|
|
3056
|
+
const mouseInit = { bubbles:true, cancelable:true, composed:true,
|
|
3057
|
+
clientX:Number(x), clientY:Number(y), button:mouseButton, detail,
|
|
3058
|
+
altKey:!!(modifiers&1), ctrlKey:!!(modifiers&2),
|
|
3059
|
+
metaKey:!!(modifiers&4), shiftKey:!!(modifiers&8) };
|
|
3060
|
+
if (options.phase) {
|
|
3061
|
+
const mask = mouseButton === 0 ? 1 : mouseButton === 1 ? 4 : 2;
|
|
3062
|
+
const before = pointerButtons;
|
|
3063
|
+
const pressed = pointerPresses.get(mouseButton);
|
|
3064
|
+
pointerButtons = options.phase === 'down' ? before | mask : before & ~mask;
|
|
3065
|
+
const Pointer = page.window.PointerEvent ?? page.window.MouseEvent;
|
|
3066
|
+
const pointer = {...mouseInit, detail:0, buttons:pointerButtons,
|
|
3067
|
+
pointerId:1, pointerType:'mouse', isPrimary:true};
|
|
3068
|
+
if (options.phase === 'down') {
|
|
3069
|
+
const record = {target, document:page.document, suppressMouse:false};
|
|
3070
|
+
pointerPresses.set(mouseButton, record);
|
|
3071
|
+
if (page.realm) {
|
|
3072
|
+
if (before === 0) suppressPointerMouse = page.realm.dispatch(target,new Pointer('pointerdown',pointer));
|
|
3073
|
+
record.suppressMouse = suppressPointerMouse;
|
|
3074
|
+
if (before !== 0) page.realm.dispatch(target,new Pointer('pointermove',pointer));
|
|
3075
|
+
if (!record.suppressMouse) {
|
|
3076
|
+
const prevented = page.realm.dispatch(target,new page.window.MouseEvent('mousedown',{...mouseInit,buttons:pointerButtons}));
|
|
3077
|
+
if (mouseButton === 0 && !prevented) focus();
|
|
3078
|
+
}
|
|
3079
|
+
if (mouseButton === 2) page.realm.dispatch(target,new Pointer('contextmenu',pointer));
|
|
3080
|
+
} else if (mouseButton === 0) focus();
|
|
3081
|
+
refresh();
|
|
3082
|
+
return null;
|
|
3083
|
+
}
|
|
3084
|
+
let selectedByDrag=false;
|
|
3085
|
+
if(mouseButton===0){
|
|
3086
|
+
if(selectionDrag){extendDrag(x,y);selectedByDrag=!selectionDrag?.element&&!page.window.getSelection().isCollapsed;}
|
|
3087
|
+
selectionDrag=null;
|
|
3088
|
+
}
|
|
3089
|
+
pointerPresses.delete(mouseButton);
|
|
3090
|
+
if (page.realm) {
|
|
3091
|
+
page.realm.dispatch(target,new Pointer(pointerButtons ? 'pointermove' : 'pointerup',pointer));
|
|
3092
|
+
if (!suppressPointerMouse) page.realm.dispatch(target,new page.window.MouseEvent('mouseup',{...mouseInit,buttons:pointerButtons}));
|
|
3093
|
+
}
|
|
3094
|
+
if (!pointerButtons) suppressPointerMouse = false;
|
|
3095
|
+
// Click/auxclick targets the common ancestor of the press and release.
|
|
3096
|
+
// Removed press targets and releases without a press do not activate.
|
|
3097
|
+
let activation = target;
|
|
3098
|
+
if (!pressed || pressed.document !== page.document || !pressed.target.isConnected) activation = null;
|
|
3099
|
+
while (activation && !activation.contains(pressed.target)) activation = activation.parentElement;
|
|
3100
|
+
refresh();
|
|
3101
|
+
return activation ? this.click(x,y,{...options,phase:undefined,activationOnly:true,activationTarget:activation,selectionDragged:selectedByDrag}) : null;
|
|
3102
|
+
}
|
|
3103
|
+
if (mouseButton !== 0) {
|
|
3104
|
+
// Non-primary clicks reach DOM handlers without primary activation
|
|
3105
|
+
// (navigation, form submission, controls, text-caret placement).
|
|
3106
|
+
if (page.realm) {
|
|
3107
|
+
const init = mouseInit;
|
|
3108
|
+
const Pointer = page.window.PointerEvent ?? page.window.MouseEvent;
|
|
3109
|
+
const pointer = {...init, detail:0, pointerId:1, pointerType:'mouse', isPrimary:true};
|
|
3110
|
+
const buttons = mouseButton === 2 ? 2 : 4;
|
|
3111
|
+
if (!options.activationOnly) {
|
|
3112
|
+
page.realm.dispatch(target,new Pointer('pointerdown',{...pointer,buttons}));
|
|
3113
|
+
page.realm.dispatch(target,new page.window.MouseEvent('mousedown',{...init,buttons}));
|
|
3114
|
+
if(mouseButton===2)page.realm.dispatch(target,new Pointer('contextmenu',{...pointer,buttons}));
|
|
3115
|
+
page.realm.dispatch(target,new Pointer('pointerup',{...pointer,buttons:0}));
|
|
3116
|
+
page.realm.dispatch(target,new page.window.MouseEvent('mouseup',{...init,buttons:0}));
|
|
3117
|
+
}
|
|
3118
|
+
page.realm.dispatch(target,new Pointer('auxclick',{...pointer,detail,buttons:pointerButtons}));
|
|
3119
|
+
refresh();
|
|
3120
|
+
}
|
|
3121
|
+
return null;
|
|
3122
|
+
}
|
|
2687
3123
|
if (page.url === "about:audio") {
|
|
2688
3124
|
const key = target.closest?.("[data-piano-note]");
|
|
2689
3125
|
const note = Number(key?.getAttribute("data-piano-note"));
|
|
@@ -2693,27 +3129,27 @@ function buildPageRenderer({
|
|
|
2693
3129
|
return null;
|
|
2694
3130
|
}
|
|
2695
3131
|
}
|
|
2696
|
-
|
|
2697
|
-
const focus = () => {
|
|
2698
|
-
const focusable = generatedDisclosure ? target : target.closest?.("input, textarea, select, button, summary, [tabindex]");
|
|
2699
|
-
if (focusable && !focusable.disabled) focusable.focus();
|
|
2700
|
-
};
|
|
2701
|
-
if (!page.realm) focus();
|
|
3132
|
+
if (!page.realm) { if (!options.activationOnly) focus(); }
|
|
2702
3133
|
else {
|
|
2703
3134
|
const controlStates = controlStatesBeforeActivation();
|
|
2704
3135
|
// A press and release, then the click (UI Events §3.5).
|
|
2705
|
-
const init = {
|
|
3136
|
+
const init = { ...mouseInit, buttons: pointerButtons };
|
|
2706
3137
|
const PointerEvent = page.window.PointerEvent ?? page.window.MouseEvent;
|
|
2707
|
-
const pointer = { ...init, pointerId: 1, pointerType: "mouse", isPrimary: true };
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
3138
|
+
const pointer = { ...init, detail: 0, pointerId: 1, pointerType: "mouse", isPrimary: true };
|
|
3139
|
+
if (!options.activationOnly) {
|
|
3140
|
+
page.realm.dispatch(target, new PointerEvent("pointerdown", { ...pointer, buttons: 1 }));
|
|
3141
|
+
if (!page.realm.dispatch(target, new page.window.MouseEvent("mousedown", { ...init, buttons: 1 }))) focus();
|
|
3142
|
+
page.realm.dispatch(target, new PointerEvent("pointerup", pointer));
|
|
3143
|
+
page.realm.dispatch(target, new page.window.MouseEvent("mouseup", init));
|
|
3144
|
+
}
|
|
3145
|
+
const prevented = page.realm.dispatch(target, new page.window.MouseEvent("click", init));
|
|
3146
|
+
// dblclick follows the second click even when its default action was cancelled.
|
|
3147
|
+
if (detail === 2) page.realm.dispatch(target, new page.window.MouseEvent("dblclick", init));
|
|
2713
3148
|
invalidateActivatedControls(controlStates);
|
|
2714
3149
|
refresh();
|
|
2715
3150
|
if (prevented) return null;
|
|
2716
3151
|
}
|
|
3152
|
+
if (options.selectionDragged) return null;
|
|
2717
3153
|
if (generatedDisclosure) {
|
|
2718
3154
|
target.open = !target.open;
|
|
2719
3155
|
refresh();
|
|
@@ -2811,12 +3247,16 @@ function buildPageRenderer({
|
|
|
2811
3247
|
},
|
|
2812
3248
|
dispose() {
|
|
2813
3249
|
if (disposedRenderer) return;
|
|
3250
|
+
resetInputCaret();
|
|
2814
3251
|
disposedRenderer = true;
|
|
2815
3252
|
loadGeneration++;
|
|
2816
3253
|
if (page.document) loadImageResource.cancelDocument(page.document);
|
|
2817
3254
|
hostClearTimeout(prepaintTimer);
|
|
2818
3255
|
hostClearTimeout(frameTimer);
|
|
2819
3256
|
disposeFrames();
|
|
3257
|
+
// qml handle begin: renderer shutdown closes application resources
|
|
3258
|
+
page.document?.__qmlHost?.dispose();
|
|
3259
|
+
// qml handle end
|
|
2820
3260
|
page.realm?.dispose();
|
|
2821
3261
|
closePiano();
|
|
2822
3262
|
disposeVideos();
|
|
@@ -2973,6 +3413,7 @@ function buildPageRenderer({
|
|
|
2973
3413
|
// Made at once, as is its initial about:blank document (HTML §4.8.5): a
|
|
2974
3414
|
// script can use its contentWindow as soon as the iframe is inserted.
|
|
2975
3415
|
const renderer = buildPageRenderer({
|
|
3416
|
+
renderMinInterval,
|
|
2976
3417
|
fetchResource: rawFetchResource,
|
|
2977
3418
|
requestNavigation: (url, downloadName) => {
|
|
2978
3419
|
if (downloadName === null) void loadFrame(frame, url);
|
|
@@ -2980,7 +3421,10 @@ function buildPageRenderer({
|
|
|
2980
3421
|
requestDownload,
|
|
2981
3422
|
reportError,
|
|
2982
3423
|
pageFetch,
|
|
3424
|
+
clipboardRead,
|
|
2983
3425
|
clipboardWrite,
|
|
3426
|
+
clipboardReadInput,
|
|
3427
|
+
clipboardWriteInput,
|
|
2984
3428
|
webSockets,
|
|
2985
3429
|
setCookie,
|
|
2986
3430
|
consoleMessage,
|
|
@@ -3141,6 +3585,12 @@ function buildPageRenderer({
|
|
|
3141
3585
|
if (resource) page.resources.set(node.domNode, resource);
|
|
3142
3586
|
else page.resources.delete(node.domNode);
|
|
3143
3587
|
}
|
|
3588
|
+
|
|
3589
|
+
// qml handle begin: texture sources keep layout/input but paint through their texture
|
|
3590
|
+
if (hasQmlDomImages(page.document)) return filterQmlTextureSources(
|
|
3591
|
+
buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf), page.renderTree);
|
|
3592
|
+
// qml handle end
|
|
3593
|
+
|
|
3144
3594
|
return buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf);
|
|
3145
3595
|
}
|
|
3146
3596
|
|
|
@@ -4411,6 +4861,11 @@ function buildPageRenderer({
|
|
|
4411
4861
|
sound.history.shift();
|
|
4412
4862
|
}
|
|
4413
4863
|
const pcm = floatToS16(taken);
|
|
4864
|
+
|
|
4865
|
+
// qml handle begin: apply QML AudioOutput gain to the output chunk only
|
|
4866
|
+
applyQmlMediaGain(pcm,qmlMediaGains.get(state.element));
|
|
4867
|
+
// qml handle end
|
|
4868
|
+
|
|
4414
4869
|
audio.write(sound.id, pcm);
|
|
4415
4870
|
sent += pcm.length;
|
|
4416
4871
|
}
|