buninu-browser 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/frontends/wayland/README.md +50 -21
- package/frontends/wayland/index.js +14 -0
- package/frontends/wayland/input.js +24 -5
- package/frontends/wayland/urlbar.js +9 -5
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- package/lib/happy-dom/inert-link.js +26 -0
- package/lib/happy-dom/node-iterator-removal.js +36 -0
- package/lib/happy-dom/node-replacement.js +60 -0
- package/lib/happy-dom/parser.js +4 -1
- package/lib/headless-shell.js +7 -4
- package/lib/layout/text-layout.js +4 -2
- package/lib/paint/display-list.js +2 -1
- package/lib/qml/README.md +456 -0
- package/lib/qml/builtins.js +76 -5
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +47 -24
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/host.js +28 -0
- package/lib/qml/loader.js +7 -3
- package/lib/qml/models.js +1 -1
- package/lib/qml/reactive.js +26 -3
- package/lib/qml/runtime.js +263 -41
- 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 +9 -1
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/textures.js +4 -1
- package/lib/qml/window-constraints.js +16 -0
- package/lib/renderer/bindings.js +110 -4
- package/lib/renderer/canvas-2d.js +16 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +14 -11
- package/lib/renderer/main.js +2 -0
- package/lib/renderer/page-realm.js +4 -9
- package/lib/renderer/page-renderer.js +283 -39
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/style/computed-style.js +8 -2
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/compare-node-iterator.js +42 -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/fractional-text-scale.js +38 -0
- package/test/fixtures/node-iterator-cases.js +59 -0
- package/test/fixtures/node-iterator-chromium.json +315 -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-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-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/fractional-text-scale.test.js +6 -0
- package/test/inert-link.test.js +37 -0
- package/test/manual-double-click.html +21 -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/node-iterator.test.js +81 -0
- package/test/page-scripts.test.js +116 -1
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas.test.js +21 -0
- package/test/qml-connections-timing.test.js +22 -0
- package/test/qml-controls-window.test.js +182 -0
- package/test/qml-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -0
- package/test/qml-locale.test.js +45 -0
- package/test/qml-nonvisual-visible.test.js +19 -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-window-update.test.js +36 -0
- package/test/render-interval.test.js +33 -0
- package/test/text-layout.test.js +15 -0
- package/test/title-no-paint.test.js +6 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +23 -0
- package/test/window-resize.test.js +29 -0
package/lib/renderer/main.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RENDER_INTERVAL_FLAG, renderMinInterval } from "./render-interval.js";
|
|
1
2
|
/**
|
|
2
3
|
* Renderer process entry point.
|
|
3
4
|
*
|
|
@@ -252,6 +253,7 @@ function clipboardWrite(text, type = 'clipboard-write') {
|
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
const renderer = await createPageRenderer({
|
|
256
|
+
renderMinInterval: renderMinInterval(process.argv.find(arg => arg.startsWith(RENDER_INTERVAL_FLAG))?.slice(RENDER_INTERVAL_FLAG.length)),
|
|
255
257
|
fetchResource,
|
|
256
258
|
rewriteScript,
|
|
257
259
|
requestNavigation: (url, download = null, fragmentFrom = null, post = null) => process.send({ type: "navigate", url, download, fragmentFrom, post }),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {selectionBinding} from './document-selection.js';
|
|
1
2
|
/**
|
|
2
3
|
* The JavaScript environment of one document.
|
|
3
4
|
*
|
|
@@ -352,6 +353,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
352
353
|
}
|
|
353
354
|
};
|
|
354
355
|
|
|
356
|
+
let documentSelection;
|
|
355
357
|
const realm = new BindingRealm(window, {
|
|
356
358
|
touch: (node, features) => {
|
|
357
359
|
if (!node || node === document || node.ownerDocument === document) hooks.touch(node, features);
|
|
@@ -567,6 +569,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
567
569
|
// HTMLCanvasElement width/height are the bitmap size (default 300×150), and
|
|
568
570
|
// getContext("2d") is how pages sample colours and draw. Other elements do
|
|
569
571
|
// not inherit these.
|
|
572
|
+
realm.selection=()=>documentSelection ??= selectionBinding(window,realm,nodeOf,()=>hooks.selectionChanged?.());
|
|
570
573
|
const canvasPrototype = elementInterfacePrototypes.get("HTMLCanvasElement");
|
|
571
574
|
for (const name of ["width", "height"]) {
|
|
572
575
|
Object.defineProperty(canvasPrototype, name, {
|
|
@@ -1785,15 +1788,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1785
1788
|
element: node,
|
|
1786
1789
|
});
|
|
1787
1790
|
},
|
|
1788
|
-
getSelection: () => (
|
|
1789
|
-
anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0,
|
|
1790
|
-
isCollapsed: true, rangeCount: 0, type: "None",
|
|
1791
|
-
addRange() {}, collapse() {}, collapseToEnd() {}, collapseToStart() {},
|
|
1792
|
-
containsNode: () => false, deleteFromDocument() {}, empty() {}, extend() {},
|
|
1793
|
-
getRangeAt: () => { throw new DOMException("Index out of range", "IndexSizeError"); },
|
|
1794
|
-
modify() {}, removeAllRanges() {}, removeRange() {}, selectAllChildren() {}, setBaseAndExtent() {},
|
|
1795
|
-
toString: () => "",
|
|
1796
|
-
}),
|
|
1791
|
+
getSelection: () => realm.selection(),
|
|
1797
1792
|
matchMedia: (query) => {
|
|
1798
1793
|
const { width, height, mobile } = hooks.viewport();
|
|
1799
1794
|
return {
|
|
@@ -29,10 +29,16 @@ import { setCanvasKit, canvasResource, observeDocumentCanvases } from "./canvas-
|
|
|
29
29
|
import { createCanvasTextRenderer, parseCanvasFont } from "./canvas-text.js";
|
|
30
30
|
import { DESKTOP_USER_AGENT } from "../user-agent.js";
|
|
31
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
|
+
|
|
32
37
|
// qml handle begin: optional document frontend
|
|
33
38
|
import {createQmlMediaController,applyQmlMediaGain} from '../qml/media-renderer.js';
|
|
34
39
|
import {INTERFACES as qmlBindings} from "./bindings.js";
|
|
35
40
|
import { enableQmlDomImages, hasQmlDomImages, captureQmlDomImage, filterQmlTextureSources, qmlTextureVisibilityChanged } from "../qml/canvas-dom.js";
|
|
41
|
+
import {loadQmlFonts} from '../qml/fonts.js';
|
|
36
42
|
import {createQmlHost} from "../qml/host.js";
|
|
37
43
|
import { loadQmlDocument } from "../qml/document.js";
|
|
38
44
|
import { isQmlDocument } from "../qml/format.js";
|
|
@@ -257,6 +263,7 @@ function buildPageRenderer({
|
|
|
257
263
|
mobile = false,
|
|
258
264
|
verifyStyle = false,
|
|
259
265
|
fullStyle = false,
|
|
266
|
+
renderMinInterval = 16,
|
|
260
267
|
// An iframe's document (see syncFrames): frames go to the embedding page,
|
|
261
268
|
// which needs only the viewport, so nothing is painted ahead.
|
|
262
269
|
embedded = false,
|
|
@@ -267,6 +274,10 @@ function buildPageRenderer({
|
|
|
267
274
|
CanvasKit, loadedFamilies, fontCollection, fontFamilies, sansSerifFamily, monospaceFamily, monospaceBoldFamily,
|
|
268
275
|
genericFamilies, builtinTypefaces,
|
|
269
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
|
+
|
|
270
281
|
let documentFonts = null;
|
|
271
282
|
let fontSheets = null;
|
|
272
283
|
let fontEnvironment = "";
|
|
@@ -330,6 +341,10 @@ function buildPageRenderer({
|
|
|
330
341
|
let framePaintTimer = null;
|
|
331
342
|
// The frame keyboard input goes to, after a click into it.
|
|
332
343
|
let focusedFrame = null;
|
|
344
|
+
let pointerButtons = 0, suppressPointerMouse = false;
|
|
345
|
+
const pointerPresses = new Map();
|
|
346
|
+
let selectionDrag = null;
|
|
347
|
+
let selectionVersion = 0;
|
|
333
348
|
const viewport = new ScrollViewport(800, 600);
|
|
334
349
|
const page = {
|
|
335
350
|
url: "about:blank",
|
|
@@ -423,6 +438,8 @@ function buildPageRenderer({
|
|
|
423
438
|
[...page.renderTree.nodesById.values()].filter(node => node.type === "text"));
|
|
424
439
|
}
|
|
425
440
|
|
|
441
|
+
// Keep fractional CSS sizes through measurement and painting. Rounding each
|
|
442
|
+
// scale independently can widen painted glyphs beyond the measured line.
|
|
426
443
|
function makeParagraph(text, computedStyle = null, fontSize = computedStyle?.fontSize ?? 16, scale = 1, foregroundPaint = null) {
|
|
427
444
|
const color = canvasColor(CanvasKit, computedStyle?.color ?? "rgba(0, 0, 0, 1)");
|
|
428
445
|
const weight = computedStyle?.fontWeight ?? 400;
|
|
@@ -445,7 +462,7 @@ function buildPageRenderer({
|
|
|
445
462
|
textStyle: {
|
|
446
463
|
color,
|
|
447
464
|
fontFamilies: families,
|
|
448
|
-
fontSize: Math.max(1,
|
|
465
|
+
fontSize: Math.max(1, fontSize) * scale,
|
|
449
466
|
fontStyle: { weight, slant, width },
|
|
450
467
|
// Roboto is a variable font; static fonts ignore the axis and match by weight.
|
|
451
468
|
fontVariations: [{ axis: "wght", value: weight }, { axis: "wdth", value: stretch }],
|
|
@@ -460,7 +477,7 @@ function buildPageRenderer({
|
|
|
460
477
|
const pushTextStyle = foregroundPaint
|
|
461
478
|
? value => builder.pushPaintStyle(value, foregroundPaint, backgroundPaint) : value => builder.pushStyle(value);
|
|
462
479
|
const textStyle = (fontFamilies) => CanvasKit.TextStyle({ color, ...decoration, fontFamilies,
|
|
463
|
-
fontSize: Math.max(1,
|
|
480
|
+
fontSize: Math.max(1, fontSize) * scale, fontStyle: { weight, slant, width },
|
|
464
481
|
fontVariations: [{ axis: "wght", value: weight }, { axis: "wdth", value: stretch }] });
|
|
465
482
|
function append(text, fonts) {
|
|
466
483
|
let emojiStyle;
|
|
@@ -503,7 +520,7 @@ function buildPageRenderer({
|
|
|
503
520
|
return paragraph;
|
|
504
521
|
}
|
|
505
522
|
|
|
506
|
-
// Text widths depend only on the font (families,
|
|
523
|
+
// Text widths depend only on the font (families, fractional size, weight),
|
|
507
524
|
// not on the element, so they are cached per font across elements,
|
|
508
525
|
// relayouts and documents. The cache is dropped when it grows too large.
|
|
509
526
|
const MAX_MEASURED_TEXTS = 200_000;
|
|
@@ -542,7 +559,7 @@ function buildPageRenderer({
|
|
|
542
559
|
const weight = computedStyle?.fontWeight ?? 400;
|
|
543
560
|
const fontStyle = computedStyle?.fontStyle ?? "normal";
|
|
544
561
|
const stretch = computedStyle?.fontStretch ?? 100;
|
|
545
|
-
const key = `${Math.max(1,
|
|
562
|
+
const key = `${Math.max(1, computedStyle?.fontSize ?? 16)}/${weight}/${fontStyle}/${stretch}/${
|
|
546
563
|
resolveFontFamilies(computedStyle?.fontFamily, weight, fontStyle, null, stretch).join(",")}`;
|
|
547
564
|
if (computedStyle && typeof computedStyle === "object") fontKeys.set(computedStyle, key);
|
|
548
565
|
return key;
|
|
@@ -589,6 +606,7 @@ function buildPageRenderer({
|
|
|
589
606
|
let rasterSurface = null;
|
|
590
607
|
let rasterSurfaceWidth = 0;
|
|
591
608
|
let rasterSurfaceHeight = 0;
|
|
609
|
+
let rasterRegion = null;
|
|
592
610
|
const rasterReadback = new RasterReadback(CanvasKit, { shared: Boolean(onFrame), allocate: allocateFramePixels });
|
|
593
611
|
// A scroll or unrelated DOM update does not change text shaping. Keep a
|
|
594
612
|
// bounded set of visible paragraphs keyed by their complete shaping input.
|
|
@@ -656,7 +674,8 @@ function buildPageRenderer({
|
|
|
656
674
|
const hints = page.hintsVisible
|
|
657
675
|
? page.hints.map((hint) => `${hint.label}@${hint.x + hint.scrollX},${hint.y + hint.scrollY}`).join(";")
|
|
658
676
|
: "";
|
|
659
|
-
|
|
677
|
+
const selection=page.window?.getSelection();
|
|
678
|
+
return hints + selectionVersion + (selection?.rangeCount ? `:${selection.anchorOffset}:${selection.focusOffset}:${selection.toString()}` : "");
|
|
660
679
|
}
|
|
661
680
|
|
|
662
681
|
function sameRasterInput() {
|
|
@@ -701,6 +720,36 @@ function buildPageRenderer({
|
|
|
701
720
|
schedulePrepaint();
|
|
702
721
|
}
|
|
703
722
|
|
|
723
|
+
/** Conservative canvas-only damage, in document CSS pixels. */
|
|
724
|
+
function canvasDamage(region) {
|
|
725
|
+
const old = rasterized.list, next = page.displayList;
|
|
726
|
+
// Reuse pixels only when geometry and every non-canvas input are stable.
|
|
727
|
+
// Transformed/fixed canvases conservatively take the full-paint path.
|
|
728
|
+
if (!rasterSurface || !Bun.deepEquals(rasterRegion, region)
|
|
729
|
+
|| rasterized.scale !== page.deviceScaleFactor || rasterized.background !== page.backgroundColor
|
|
730
|
+
|| rasterized.overlay !== overlayKey() || rasterized.frames !== framesVersion
|
|
731
|
+
|| rasterized.scrollX !== viewport.x || rasterized.scrollY !== viewport.y
|
|
732
|
+
|| !old || !next || old.images.length !== next.images.length
|
|
733
|
+
|| !Bun.deepEquals(old.items, next.items)
|
|
734
|
+
|| next.items.some(item => item.op === 'drawVideo')) return null;
|
|
735
|
+
let damage = null;
|
|
736
|
+
for (let i = 0; i < next.images.length; i++) {
|
|
737
|
+
if (old.images[i] === next.images[i]) continue;
|
|
738
|
+
const uses = next.items.filter(item => item.image === i);
|
|
739
|
+
if (!uses.length) return null;
|
|
740
|
+
for (const item of uses) {
|
|
741
|
+
if (item.op !== 'drawImage' || item.fixed || item.transform
|
|
742
|
+
|| page.renderTree.nodesById.get(item.nodeId)?.domNode?.tagName !== 'CANVAS') return null;
|
|
743
|
+
const r = item.rect;
|
|
744
|
+
if (![r.x, r.y, r.width, r.height].every(Number.isFinite)) return null;
|
|
745
|
+
const left = r.x - 1, top = r.y - 1, right = r.x + r.width + 1, bottom = r.y + r.height + 1;
|
|
746
|
+
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)}
|
|
747
|
+
: {x:left,y:top,right,bottom};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return damage;
|
|
751
|
+
}
|
|
752
|
+
|
|
704
753
|
/** Rasterizes a page rectangle (CSS pixels) into RGBA pixels. */
|
|
705
754
|
function rasterTile(region) {
|
|
706
755
|
// Prepaint can run before a queued canvas/image repaint. Refresh native
|
|
@@ -712,6 +761,8 @@ function buildPageRenderer({
|
|
|
712
761
|
const scale = page.deviceScaleFactor;
|
|
713
762
|
const pixelWidth = Math.max(1, Math.round(region.width * scale));
|
|
714
763
|
const pixelHeight = Math.max(1, Math.round(region.height * scale));
|
|
764
|
+
const damage = canvasDamage(region);
|
|
765
|
+
if (damage) trace?.(`${page.url}: canvas damage ${damage.right-damage.x}x${damage.bottom-damage.y}`);
|
|
715
766
|
if (!rasterSurface || rasterSurfaceWidth !== pixelWidth || rasterSurfaceHeight !== pixelHeight) {
|
|
716
767
|
// MakeSurface owns a malloc-backed pixel buffer; delete() alone leaks it.
|
|
717
768
|
rasterSurface?.dispose();
|
|
@@ -722,9 +773,19 @@ function buildPageRenderer({
|
|
|
722
773
|
const surface = rasterSurface;
|
|
723
774
|
if (!surface) throw new Error(`Unable to create a ${pixelWidth}x${pixelHeight} CanvasKit surface`);
|
|
724
775
|
const canvas = surface.getCanvas();
|
|
776
|
+
canvas.save();
|
|
777
|
+
if (damage) canvas.clipRect(CanvasKit.LTRBRect(
|
|
778
|
+
Math.floor((damage.x-region.x)*scale), Math.floor((damage.y-region.y)*scale),
|
|
779
|
+
Math.ceil((damage.right-region.x)*scale), Math.ceil((damage.bottom-region.y)*scale),
|
|
780
|
+
), CanvasKit.ClipOp.Intersect, false);
|
|
781
|
+
// Clear and replay all layers inside the damage, including backgrounds
|
|
782
|
+
// and foreground occluders. Transparent canvas pixels must not leave trails.
|
|
725
783
|
canvas.clear(canvasColor(CanvasKit, page.backgroundColor));
|
|
726
|
-
paintDisplayList(canvas, page.displayList, region, scale
|
|
784
|
+
paintDisplayList(canvas, page.displayList, region, scale, null, damage ? {
|
|
785
|
+
x:damage.x, y:damage.y, width:damage.right-damage.x, height:damage.bottom-damage.y,
|
|
786
|
+
} : region);
|
|
727
787
|
drawHints(canvas, region);
|
|
788
|
+
canvas.restore();
|
|
728
789
|
surface.flush();
|
|
729
790
|
const image = surface.makeImageSnapshot();
|
|
730
791
|
let pixels;
|
|
@@ -732,6 +793,7 @@ function buildPageRenderer({
|
|
|
732
793
|
finally { image.delete(); }
|
|
733
794
|
const videoLayer = makeVideoLayer(canvas, surface, region, scale, pixelWidth, pixelHeight);
|
|
734
795
|
page.needsPaint = false;
|
|
796
|
+
rasterRegion = {...region};
|
|
735
797
|
rasterized = {
|
|
736
798
|
list: page.displayList, overlay: overlayKey(), background: page.backgroundColor, scale: page.deviceScaleFactor,
|
|
737
799
|
scrollX: viewport.x, scrollY: viewport.y, frames: framesVersion,
|
|
@@ -807,10 +869,10 @@ function buildPageRenderer({
|
|
|
807
869
|
frameTimer = hostSetTimeout(() => {
|
|
808
870
|
frameTimer = null;
|
|
809
871
|
refresh();
|
|
810
|
-
// Coalesce ordinary mutations for
|
|
811
|
-
// retain a longer idle window so related script mutations can settle
|
|
872
|
+
// Coalesce ordinary mutations for the configured minimum interval.
|
|
873
|
+
// Expensive style/layout rebuilds retain a longer idle window so related script mutations can settle
|
|
812
874
|
// instead of continuously rebuilding a large document.
|
|
813
|
-
}, Math.max(
|
|
875
|
+
}, Math.max(renderMinInterval, lastRebuildMs * 2));
|
|
814
876
|
}
|
|
815
877
|
|
|
816
878
|
/** The viewport as an encoded image (base64), cut from the latest tile. */
|
|
@@ -830,7 +892,7 @@ function buildPageRenderer({
|
|
|
830
892
|
/** Rasterizes display items intersecting the viewport onto a CanvasKit canvas. */
|
|
831
893
|
const paintFailures = new Set();
|
|
832
894
|
|
|
833
|
-
function paintDisplayList(canvas, displayList, view, scale, videoOverride = null) {
|
|
895
|
+
function paintDisplayList(canvas, displayList, view, scale, videoOverride = null, dirtyView = view) {
|
|
834
896
|
if (!displayList) return;
|
|
835
897
|
const pixel = (value) => Math.round(value * scale);
|
|
836
898
|
const deviceRect = (rect) => CanvasKit.XYWHRect(
|
|
@@ -857,7 +919,7 @@ function buildPageRenderer({
|
|
|
857
919
|
if (rounded) canvas.drawRRect(rounded, paint);
|
|
858
920
|
else canvas.drawRect(deviceRect(rect), paint);
|
|
859
921
|
};
|
|
860
|
-
for (const item of visibleItems(displayList,
|
|
922
|
+
for (const item of visibleItems(displayList, dirtyView, viewport)) {
|
|
861
923
|
const saved = item.fixed || item.clipRect || item.opacity < 1 || item.transform ? canvas.save() : null;
|
|
862
924
|
try {
|
|
863
925
|
if (item.fixed) canvas.translate(viewport.x * scale, viewport.y * scale);
|
|
@@ -1046,6 +1108,17 @@ function buildPageRenderer({
|
|
|
1046
1108
|
} else {
|
|
1047
1109
|
canvas.drawParagraph(entry.paragraph, pixel(item.x - view.x), pixel(item.y - view.y));
|
|
1048
1110
|
}
|
|
1111
|
+
const selected=selectedTextRange(item);
|
|
1112
|
+
if(selected){
|
|
1113
|
+
const boxes=entry.paragraph.getRectsForRange(selected.from,selected.to,CanvasKit.RectHeightStyle.Max,CanvasKit.RectWidthStyle.Tight);
|
|
1114
|
+
const white=paragraphForPaint({...item,color:'rgba(255, 255, 255, 1)',gradient:null},scale,pixel(Math.max(1,item.textWidth+1)));
|
|
1115
|
+
const paint=new CanvasKit.Paint();paint.setColor(canvasColor(CanvasKit,'rgba(56, 116, 242, 1)'));
|
|
1116
|
+
try{for(const box of boxes){
|
|
1117
|
+
const r=box.rect,ox=pixel(item.x-view.x),oy=pixel(item.y-view.y);
|
|
1118
|
+
const rect=CanvasKit.LTRBRect(r[0]+ox,r[1]+oy,r[2]+ox,r[3]+oy);
|
|
1119
|
+
canvas.save();canvas.clipRect(rect,CanvasKit.ClipOp.Intersect,false);canvas.drawRect(rect,paint);canvas.drawParagraph(white.paragraph,ox,oy);canvas.restore();
|
|
1120
|
+
}}finally{paint.delete();if(!white.retained)white.paragraph.delete();}
|
|
1121
|
+
}
|
|
1049
1122
|
} finally {
|
|
1050
1123
|
if (!entry.retained) entry.paragraph.delete();
|
|
1051
1124
|
}
|
|
@@ -1592,6 +1665,7 @@ function buildPageRenderer({
|
|
|
1592
1665
|
for (const image of page.decodedImages) image.delete();
|
|
1593
1666
|
disposeBackgrounds();
|
|
1594
1667
|
page.window = parsed.window;
|
|
1668
|
+
pointerButtons = 0; suppressPointerMouse = false; pointerPresses.clear(); selectionDrag=null;
|
|
1595
1669
|
page.document = parsed.document;
|
|
1596
1670
|
observeDocumentCanvases(parsed.document, (node) => {
|
|
1597
1671
|
if (disposedRenderer || node.ownerDocument !== page.document || !node.isConnected) return;
|
|
@@ -1793,6 +1867,16 @@ function buildPageRenderer({
|
|
|
1793
1867
|
onLifecycle?.("DOMContentLoaded", generation);
|
|
1794
1868
|
onLifecycle?.("load", generation);
|
|
1795
1869
|
};
|
|
1870
|
+
// qml handle begin: optional local Qt font directory, never inherited by HTML
|
|
1871
|
+
const selectedFonts = page.scripting === "host" && url.startsWith("file:") && isQmlDocument(url, contentType)
|
|
1872
|
+
? await loadQmlFonts(standardFonts, message => reportError(new Error(message))) : standardFonts;
|
|
1873
|
+
if (!current()) return;
|
|
1874
|
+
if (fontCollection !== selectedFonts.fontCollection) {
|
|
1875
|
+
({loadedFamilies, fontCollection, fontFamilies, builtinTypefaces} = selectedFonts);
|
|
1876
|
+
clearFontCaches();
|
|
1877
|
+
}
|
|
1878
|
+
// qml handle end
|
|
1879
|
+
|
|
1796
1880
|
page.url = url;
|
|
1797
1881
|
page.cookie = typeof cookie === "string" ? cookie : "";
|
|
1798
1882
|
if (error) {
|
|
@@ -1931,6 +2015,16 @@ function buildPageRenderer({
|
|
|
1931
2015
|
}
|
|
1932
2016
|
// qml handle end
|
|
1933
2017
|
|
|
2018
|
+
// qml handle begin: local QML fragment shader capability
|
|
2019
|
+
if (page.document.__qmlHost) {
|
|
2020
|
+
const host=page.document.__qmlHost;
|
|
2021
|
+
host.shader=createShaderRenderer({kit:CanvasKit,unwrap:e=>qmlBindings.Node.unwrap(e),
|
|
2022
|
+
valid:e=>e?.ownerDocument===page.document,
|
|
2023
|
+
resource:e=>e.tagName==='CANVAS'?canvasResource(e):page.resources.get(e),
|
|
2024
|
+
paint:paintQmlCanvas,track:fn=>host.trackResource(fn)});
|
|
2025
|
+
}
|
|
2026
|
+
// qml handle end
|
|
2027
|
+
|
|
1934
2028
|
await page.realm.runDocumentScripts();
|
|
1935
2029
|
if (!current()) return;
|
|
1936
2030
|
phase("scripts");
|
|
@@ -2129,6 +2223,54 @@ function buildPageRenderer({
|
|
|
2129
2223
|
return Math.max(0,Math.min(String(element.value).length,position));
|
|
2130
2224
|
}
|
|
2131
2225
|
|
|
2226
|
+
function selectableText(node){
|
|
2227
|
+
for(let el=node.parentElement;el;el=el.parentElement){
|
|
2228
|
+
const value=styleEngine.get(el)?.userSelect;
|
|
2229
|
+
if(value==='none')return false;
|
|
2230
|
+
if(value==='text'||value==='all')return true;
|
|
2231
|
+
}
|
|
2232
|
+
return true;
|
|
2233
|
+
}
|
|
2234
|
+
function documentTextPoint(x,y,target=null) {
|
|
2235
|
+
let best=null;
|
|
2236
|
+
for(const hit of page.displayList?.hits??[]){
|
|
2237
|
+
const run=hit.textRun,node=page.renderTree.nodesById.get(hit.nodeId)?.domNode;
|
|
2238
|
+
if(!run||node?.nodeType!==3||!node.isConnected||!selectableText(node))continue;
|
|
2239
|
+
if(target && !target.contains(node))continue;
|
|
2240
|
+
if(node.parentElement?.closest('button,input,textarea,select,canvas,script,style'))continue;
|
|
2241
|
+
let px=x+(hit.fixed?0:viewport.x),py=y+(hit.fixed?0:viewport.y);
|
|
2242
|
+
if(hit.transform){const {a,d,e,f}=hit.transform;if(!a||!d)continue;px=(px-e)/a;py=(py-f)/d;}
|
|
2243
|
+
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;
|
|
2244
|
+
const height=parseFloat(run.style.lineHeight)||parseFloat(run.style.fontSize)||16;
|
|
2245
|
+
const dy=Math.max(run.y-py,py-run.y-height,0),dx=Math.max(run.x-px,px-run.x-run.width,0);
|
|
2246
|
+
if(!best||dy<best.dy||dy===best.dy&&dx<best.dx)best={run,node,px,dy,dx};
|
|
2247
|
+
}
|
|
2248
|
+
if(!best)return null;
|
|
2249
|
+
const {run,node,px}=best,p=makeParagraph(run.text,run.style);
|
|
2250
|
+
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))};}
|
|
2251
|
+
finally{p.delete();}
|
|
2252
|
+
}
|
|
2253
|
+
function extendDrag(x,y){
|
|
2254
|
+
if(selectionDrag.element){
|
|
2255
|
+
const el=selectionDrag.element;if(!el.isConnected||page.document.activeElement!==el){selectionDrag=null;return;}
|
|
2256
|
+
selectInput(el,selectionDrag.anchor,inputPositionAtPoint(el,x,y));syncInputCaret();invalidateInput();
|
|
2257
|
+
}else{
|
|
2258
|
+
const point=documentTextPoint(x,y);if(!point||!selectionDrag.node.isConnected)return;
|
|
2259
|
+
page.window.getSelection().setBaseAndExtent(selectionDrag.node,selectionDrag.offset,point.node,point.offset);
|
|
2260
|
+
selectionVersion++;
|
|
2261
|
+
page.needsPaint=true;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
function selectedTextRange(item){
|
|
2265
|
+
const sel=page.window.getSelection(),node=page.renderTree.nodesById.get(item.nodeId)?.domNode;
|
|
2266
|
+
if(!sel?.rangeCount||sel.isCollapsed||node?.nodeType!==3||!node.isConnected||!selectableText(node))return null;
|
|
2267
|
+
const range=sel.getRangeAt(0);if(!range.intersectsNode(node))return null;
|
|
2268
|
+
const base=item.sourceStart??0;
|
|
2269
|
+
const from=Math.max(0,(range.startContainer===node?range.startOffset:0)-base);
|
|
2270
|
+
const to=Math.min(item.text.length,(range.endContainer===node?range.endOffset:node.textContent.length)-base);
|
|
2271
|
+
return to>from?{from,to}:null;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2132
2274
|
function clipboardForInput(element,operation,backend={read:clipboardReadInput,write:clipboardWriteInput}) {
|
|
2133
2275
|
return inputClipboard(element,operation,{...backend,replace:(text,type)=>{
|
|
2134
2276
|
if(page.document.activeElement!==element)return;
|
|
@@ -2298,6 +2440,7 @@ function buildPageRenderer({
|
|
|
2298
2440
|
}
|
|
2299
2441
|
},
|
|
2300
2442
|
whenImagesLoaded: () => Promise.all([page.imagesLoaded, documentFonts.ready(), ...[...page.backgrounds.values()].map((entry) => entry.loaded)]),
|
|
2443
|
+
selectionChanged(){selectionVersion++;page.needsPaint=true;scheduleFrame();},
|
|
2301
2444
|
afterTask() {},
|
|
2302
2445
|
boundsOf(element) {
|
|
2303
2446
|
if (page.dirty) rebuildRenderTree("script read geometry");
|
|
@@ -2645,6 +2788,10 @@ function buildPageRenderer({
|
|
|
2645
2788
|
? deviceScaleFactor
|
|
2646
2789
|
: 1;
|
|
2647
2790
|
viewport.resize(width, height);
|
|
2791
|
+
// Viewport dimensions are layout input even when DOM/stylesheets did
|
|
2792
|
+
// not mutate (including CSS resizes that keep the same pixel size by
|
|
2793
|
+
// changing DPR). Bypass retained-layout shortcuts in that case.
|
|
2794
|
+
if (resized) page.resourceLayoutDirty = true;
|
|
2648
2795
|
rebuildRenderTree();
|
|
2649
2796
|
// Listeners must observe the new viewport and layout. A repeated metrics
|
|
2650
2797
|
// request (or DPR-only change) does not resize the CSS viewport.
|
|
@@ -2727,9 +2874,9 @@ function buildPageRenderer({
|
|
|
2727
2874
|
schedulePrepaint();
|
|
2728
2875
|
},
|
|
2729
2876
|
title() {
|
|
2730
|
-
|
|
2731
|
-
//
|
|
2732
|
-
return page.document?.title
|
|
2877
|
+
// DOM title changes are synchronous and do not require layout or paint.
|
|
2878
|
+
// An empty document title is also a live value, not a cached-title fallback.
|
|
2879
|
+
return page.document?.title ?? page.title;
|
|
2733
2880
|
},
|
|
2734
2881
|
scroll(deltaX, deltaY) {
|
|
2735
2882
|
if (viewport.scrollBy(deltaX, deltaY)) render();
|
|
@@ -2755,6 +2902,9 @@ function buildPageRenderer({
|
|
|
2755
2902
|
const prevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keydown", init));
|
|
2756
2903
|
let activated = false;
|
|
2757
2904
|
const inputHandled=!prevented&&inputDefault(target,init);
|
|
2905
|
+
if(!prevented&&!inputHandled&&(init.ctrlKey||init.metaKey)&&init.key.toLowerCase()==='c'){
|
|
2906
|
+
const text=page.window.getSelection().toString();if(text)clipboardWriteInput(text).catch(reportError);
|
|
2907
|
+
}
|
|
2758
2908
|
if (!inputHandled && !prevented && !init.ctrlKey && !init.metaKey && !init.altKey) {
|
|
2759
2909
|
if ((key === "Enter" || key === " ") && isDisclosureControl(target)) {
|
|
2760
2910
|
activated = true;
|
|
@@ -2824,18 +2974,29 @@ function buildPageRenderer({
|
|
|
2824
2974
|
return wheel(Number(x), Number(y), Number(deltaX) || 0, Number(deltaY) || 0, options);
|
|
2825
2975
|
},
|
|
2826
2976
|
/** Move the pointer in viewport coordinates, including boundary events. */
|
|
2827
|
-
move(x, y) {
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2977
|
+
move(x, y, options = {}) {
|
|
2978
|
+
// Hit testing needs current geometry, not a rasterized frame. Dispatch
|
|
2979
|
+
// every motion (drawing needs every point), but let the frame scheduler
|
|
2980
|
+
// combine their paints instead of rasterizing the whole page per event.
|
|
2981
|
+
if (page.dirty) rebuildRenderTree("pointer hit test");
|
|
2982
|
+
const target = elementAtPoint(page.renderTree, page.displayList, Number(x), Number(y), viewport) ?? (pointerButtons ? page.document.documentElement : null);
|
|
2983
|
+
const inside = target?.tagName === 'IFRAME' ? frameAt(target, x, y) : null;
|
|
2984
|
+
if (inside) inside.frame.renderer.move(inside.x, inside.y, options);
|
|
2985
|
+
dispatchPointerMove(page.window, page.realm, target, Number(x), Number(y), {...options, buttons: pointerButtons, suppressMouse:suppressPointerMouse});
|
|
2986
|
+
if(selectionDrag && (pointerButtons&1)) extendDrag(x,y);
|
|
2987
|
+
if (page.dirty || page.needsPaint) scheduleFrame();
|
|
2832
2988
|
},
|
|
2833
2989
|
/**
|
|
2834
2990
|
* Clicks at a viewport point: the page receives a click event first, and
|
|
2835
2991
|
* unless it calls preventDefault() a link under the point is returned for
|
|
2836
2992
|
* the controller to navigate to.
|
|
2837
2993
|
*/
|
|
2838
|
-
|
|
2994
|
+
pointerButton(x, y, button, state, options = {}) {
|
|
2995
|
+
if (button < 272 || button > 274) return null;
|
|
2996
|
+
return this.click(x, y, {...options, button: button === 273 ? 'right' : button === 274 ? 'middle' : 'left', phase: state ? 'down' : 'up'});
|
|
2997
|
+
},
|
|
2998
|
+
click(x, y, options = {}) {
|
|
2999
|
+
const detail = Number.isInteger(options.clickCount) && options.clickCount > 0 ? options.clickCount : 1;
|
|
2839
3000
|
refresh();
|
|
2840
3001
|
let region = hitTest(page.renderTree, page.layout, x, y, viewport, viewport);
|
|
2841
3002
|
let forwarded = false;
|
|
@@ -2861,7 +3022,7 @@ function buildPageRenderer({
|
|
|
2861
3022
|
// and a search form's forwarded field takes the click on the form.
|
|
2862
3023
|
const hitNode = page.displayList ? renderNodeAtPoint(page.renderTree, page.displayList, x, y, viewport) : null;
|
|
2863
3024
|
const precise = hitNode?.domNode?.nodeType === 1 ? hitNode.domNode : hitNode?.domNode?.parentElement;
|
|
2864
|
-
const target = forwarded ? region.element : inputOwner.get(precise) ?? precise ?? region?.element ?? null;
|
|
3025
|
+
const target = options.activationTarget ?? (forwarded ? region.element : inputOwner.get(precise) ?? precise ?? region?.element ?? (options.phase ? page.document.documentElement : null));
|
|
2865
3026
|
const generatedDisclosure = hitNode?.pseudo === "summary" && target?.tagName === "DETAILS";
|
|
2866
3027
|
if (!target) {
|
|
2867
3028
|
focusedFrame = null;
|
|
@@ -2874,11 +3035,97 @@ function buildPageRenderer({
|
|
|
2874
3035
|
const inFrame = target.tagName === "IFRAME" ? frameAt(target, x, y) : null;
|
|
2875
3036
|
focusedFrame = inFrame?.frame ?? null;
|
|
2876
3037
|
if (inFrame) {
|
|
2877
|
-
const followed = inFrame.frame.renderer.click(inFrame.x, inFrame.y);
|
|
3038
|
+
const followed = inFrame.frame.renderer.click(inFrame.x, inFrame.y, options);
|
|
2878
3039
|
const href = typeof followed === "string" ? followed : followed?.fragmentFrom ? null : followed?.url;
|
|
2879
3040
|
if (href && !followed?.download) void loadFrame(inFrame.frame, href);
|
|
2880
3041
|
return null;
|
|
2881
3042
|
}
|
|
3043
|
+
const mouseButton = options.button === 'right' || options.button === 2 ? 2
|
|
3044
|
+
: options.button === 'middle' || options.button === 1 ? 1 : 0;
|
|
3045
|
+
// The press focuses the focusable element it lands in, unless mousedown was canceled.
|
|
3046
|
+
const focus = () => {
|
|
3047
|
+
selectionDrag=null;
|
|
3048
|
+
const focusable = generatedDisclosure ? target : target.closest?.("input, textarea, select, button, summary, [tabindex]");
|
|
3049
|
+
if (focusable && !focusable.disabled) {
|
|
3050
|
+
const position=isTextInput(focusable)&&!forwarded?inputPositionAtPoint(focusable,x,y):null;
|
|
3051
|
+
focusable.focus();
|
|
3052
|
+
if(position!==null){selectInput(focusable,position);syncInputCaret();invalidateInput();if(options.phase==='down')selectionDrag={element:focusable,anchor:position};}
|
|
3053
|
+
} else if(mouseButton===0){
|
|
3054
|
+
const point=documentTextPoint(x,y,target);
|
|
3055
|
+
if(point)page.document.activeElement?.blur?.();
|
|
3056
|
+
selectionVersion++;page.window.getSelection().removeAllRanges();
|
|
3057
|
+
if(point){page.window.getSelection().collapse(point.node,point.offset);if(options.phase==='down')selectionDrag=point;}
|
|
3058
|
+
page.needsPaint=true;
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
3061
|
+
const modifiers = options.modifiers ?? 0;
|
|
3062
|
+
const mouseInit = { bubbles:true, cancelable:true, composed:true,
|
|
3063
|
+
clientX:Number(x), clientY:Number(y), button:mouseButton, detail,
|
|
3064
|
+
altKey:!!(modifiers&1), ctrlKey:!!(modifiers&2),
|
|
3065
|
+
metaKey:!!(modifiers&4), shiftKey:!!(modifiers&8) };
|
|
3066
|
+
if (options.phase) {
|
|
3067
|
+
const mask = mouseButton === 0 ? 1 : mouseButton === 1 ? 4 : 2;
|
|
3068
|
+
const before = pointerButtons;
|
|
3069
|
+
const pressed = pointerPresses.get(mouseButton);
|
|
3070
|
+
pointerButtons = options.phase === 'down' ? before | mask : before & ~mask;
|
|
3071
|
+
const Pointer = page.window.PointerEvent ?? page.window.MouseEvent;
|
|
3072
|
+
const pointer = {...mouseInit, detail:0, buttons:pointerButtons,
|
|
3073
|
+
pointerId:1, pointerType:'mouse', isPrimary:true};
|
|
3074
|
+
if (options.phase === 'down') {
|
|
3075
|
+
const record = {target, document:page.document, suppressMouse:false};
|
|
3076
|
+
pointerPresses.set(mouseButton, record);
|
|
3077
|
+
if (page.realm) {
|
|
3078
|
+
if (before === 0) suppressPointerMouse = page.realm.dispatch(target,new Pointer('pointerdown',pointer));
|
|
3079
|
+
record.suppressMouse = suppressPointerMouse;
|
|
3080
|
+
if (before !== 0) page.realm.dispatch(target,new Pointer('pointermove',pointer));
|
|
3081
|
+
if (!record.suppressMouse) {
|
|
3082
|
+
const prevented = page.realm.dispatch(target,new page.window.MouseEvent('mousedown',{...mouseInit,buttons:pointerButtons}));
|
|
3083
|
+
if (mouseButton === 0 && !prevented) focus();
|
|
3084
|
+
}
|
|
3085
|
+
if (mouseButton === 2) page.realm.dispatch(target,new Pointer('contextmenu',pointer));
|
|
3086
|
+
} else if (mouseButton === 0) focus();
|
|
3087
|
+
refresh();
|
|
3088
|
+
return null;
|
|
3089
|
+
}
|
|
3090
|
+
let selectedByDrag=false;
|
|
3091
|
+
if(mouseButton===0){
|
|
3092
|
+
if(selectionDrag){extendDrag(x,y);selectedByDrag=!selectionDrag?.element&&!page.window.getSelection().isCollapsed;}
|
|
3093
|
+
selectionDrag=null;
|
|
3094
|
+
}
|
|
3095
|
+
pointerPresses.delete(mouseButton);
|
|
3096
|
+
if (page.realm) {
|
|
3097
|
+
page.realm.dispatch(target,new Pointer(pointerButtons ? 'pointermove' : 'pointerup',pointer));
|
|
3098
|
+
if (!suppressPointerMouse) page.realm.dispatch(target,new page.window.MouseEvent('mouseup',{...mouseInit,buttons:pointerButtons}));
|
|
3099
|
+
}
|
|
3100
|
+
if (!pointerButtons) suppressPointerMouse = false;
|
|
3101
|
+
// Click/auxclick targets the common ancestor of the press and release.
|
|
3102
|
+
// Removed press targets and releases without a press do not activate.
|
|
3103
|
+
let activation = target;
|
|
3104
|
+
if (!pressed || pressed.document !== page.document || !pressed.target.isConnected) activation = null;
|
|
3105
|
+
while (activation && !activation.contains(pressed.target)) activation = activation.parentElement;
|
|
3106
|
+
refresh();
|
|
3107
|
+
return activation ? this.click(x,y,{...options,phase:undefined,activationOnly:true,activationTarget:activation,selectionDragged:selectedByDrag}) : null;
|
|
3108
|
+
}
|
|
3109
|
+
if (mouseButton !== 0) {
|
|
3110
|
+
// Non-primary clicks reach DOM handlers without primary activation
|
|
3111
|
+
// (navigation, form submission, controls, text-caret placement).
|
|
3112
|
+
if (page.realm) {
|
|
3113
|
+
const init = mouseInit;
|
|
3114
|
+
const Pointer = page.window.PointerEvent ?? page.window.MouseEvent;
|
|
3115
|
+
const pointer = {...init, detail:0, pointerId:1, pointerType:'mouse', isPrimary:true};
|
|
3116
|
+
const buttons = mouseButton === 2 ? 2 : 4;
|
|
3117
|
+
if (!options.activationOnly) {
|
|
3118
|
+
page.realm.dispatch(target,new Pointer('pointerdown',{...pointer,buttons}));
|
|
3119
|
+
page.realm.dispatch(target,new page.window.MouseEvent('mousedown',{...init,buttons}));
|
|
3120
|
+
if(mouseButton===2)page.realm.dispatch(target,new Pointer('contextmenu',{...pointer,buttons}));
|
|
3121
|
+
page.realm.dispatch(target,new Pointer('pointerup',{...pointer,buttons:0}));
|
|
3122
|
+
page.realm.dispatch(target,new page.window.MouseEvent('mouseup',{...init,buttons:0}));
|
|
3123
|
+
}
|
|
3124
|
+
page.realm.dispatch(target,new Pointer('auxclick',{...pointer,detail,buttons:pointerButtons}));
|
|
3125
|
+
refresh();
|
|
3126
|
+
}
|
|
3127
|
+
return null;
|
|
3128
|
+
}
|
|
2882
3129
|
if (page.url === "about:audio") {
|
|
2883
3130
|
const key = target.closest?.("[data-piano-note]");
|
|
2884
3131
|
const note = Number(key?.getAttribute("data-piano-note"));
|
|
@@ -2888,31 +3135,27 @@ function buildPageRenderer({
|
|
|
2888
3135
|
return null;
|
|
2889
3136
|
}
|
|
2890
3137
|
}
|
|
2891
|
-
|
|
2892
|
-
const focus = () => {
|
|
2893
|
-
const focusable = generatedDisclosure ? target : target.closest?.("input, textarea, select, button, summary, [tabindex]");
|
|
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
|
-
}
|
|
2899
|
-
};
|
|
2900
|
-
if (!page.realm) focus();
|
|
3138
|
+
if (!page.realm) { if (!options.activationOnly) focus(); }
|
|
2901
3139
|
else {
|
|
2902
3140
|
const controlStates = controlStatesBeforeActivation();
|
|
2903
3141
|
// A press and release, then the click (UI Events §3.5).
|
|
2904
|
-
const init = {
|
|
3142
|
+
const init = { ...mouseInit, buttons: pointerButtons };
|
|
2905
3143
|
const PointerEvent = page.window.PointerEvent ?? page.window.MouseEvent;
|
|
2906
|
-
const pointer = { ...init, pointerId: 1, pointerType: "mouse", isPrimary: true };
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
3144
|
+
const pointer = { ...init, detail: 0, pointerId: 1, pointerType: "mouse", isPrimary: true };
|
|
3145
|
+
if (!options.activationOnly) {
|
|
3146
|
+
page.realm.dispatch(target, new PointerEvent("pointerdown", { ...pointer, buttons: 1 }));
|
|
3147
|
+
if (!page.realm.dispatch(target, new page.window.MouseEvent("mousedown", { ...init, buttons: 1 }))) focus();
|
|
3148
|
+
page.realm.dispatch(target, new PointerEvent("pointerup", pointer));
|
|
3149
|
+
page.realm.dispatch(target, new page.window.MouseEvent("mouseup", init));
|
|
3150
|
+
}
|
|
3151
|
+
const prevented = page.realm.dispatch(target, new page.window.MouseEvent("click", init));
|
|
3152
|
+
// dblclick follows the second click even when its default action was cancelled.
|
|
3153
|
+
if (detail === 2) page.realm.dispatch(target, new page.window.MouseEvent("dblclick", init));
|
|
2912
3154
|
invalidateActivatedControls(controlStates);
|
|
2913
3155
|
refresh();
|
|
2914
3156
|
if (prevented) return null;
|
|
2915
3157
|
}
|
|
3158
|
+
if (options.selectionDragged) return null;
|
|
2916
3159
|
if (generatedDisclosure) {
|
|
2917
3160
|
target.open = !target.open;
|
|
2918
3161
|
refresh();
|
|
@@ -3176,6 +3419,7 @@ function buildPageRenderer({
|
|
|
3176
3419
|
// Made at once, as is its initial about:blank document (HTML §4.8.5): a
|
|
3177
3420
|
// script can use its contentWindow as soon as the iframe is inserted.
|
|
3178
3421
|
const renderer = buildPageRenderer({
|
|
3422
|
+
renderMinInterval,
|
|
3179
3423
|
fetchResource: rawFetchResource,
|
|
3180
3424
|
requestNavigation: (url, downloadName) => {
|
|
3181
3425
|
if (downloadName === null) void loadFrame(frame, url);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const previousTargets = new WeakMap();
|
|
2
2
|
|
|
3
3
|
/** Deliver pointer/mouse boundary events before movement, as a real pointer does. */
|
|
4
|
-
export function dispatchPointerMove(window, realm, target, x, y) {
|
|
4
|
+
export function dispatchPointerMove(window, realm, target, x, y, options = {}) {
|
|
5
5
|
const document = window.document;
|
|
6
6
|
const previous = previousTargets.get(document) ?? null;
|
|
7
7
|
previousTargets.set(document, target);
|
|
@@ -12,7 +12,9 @@ export function dispatchPointerMove(window, realm, target, x, y) {
|
|
|
12
12
|
const pointer = type.startsWith('pointer');
|
|
13
13
|
const Constructor = pointer ? window.PointerEvent : window.MouseEvent;
|
|
14
14
|
const event = new Constructor(type, { clientX: x, clientY: y, relatedTarget, bubbles,
|
|
15
|
-
composed: bubbles, cancelable: bubbles, button: pointer ? -1 : 0, buttons: 0,
|
|
15
|
+
composed: bubbles, cancelable: bubbles, button: pointer ? -1 : 0, buttons: options.buttons ?? 0,
|
|
16
|
+
altKey:!!(options.modifiers&1),ctrlKey:!!(options.modifiers&2),
|
|
17
|
+
metaKey:!!(options.modifiers&4),shiftKey:!!(options.modifiers&8),
|
|
16
18
|
pointerId: 1, pointerType: 'mouse', isPrimary: true });
|
|
17
19
|
if (realm) realm.dispatch(node, event); else node.dispatchEvent(event);
|
|
18
20
|
};
|
|
@@ -25,5 +27,5 @@ export function dispatchPointerMove(window, realm, target, x, y) {
|
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
send(target, 'pointermove', null);
|
|
28
|
-
send(target, 'mousemove', null);
|
|
30
|
+
if (!options.suppressMouse) send(target, 'mousemove', null);
|
|
29
31
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const RENDER_INTERVAL_FLAG = "--buninu-render-min-interval=";
|
|
2
|
+
|
|
3
|
+
// Validate in the controller, then pass only this number to the sandboxed child.
|
|
4
|
+
export function renderMinInterval(value, warn = console.warn) {
|
|
5
|
+
if (value === undefined) return 16;
|
|
6
|
+
if (/^\d+$/.test(value) && Number(value) <= 60000) return Number(value);
|
|
7
|
+
warn("buninu-browser: BUNINU_RENDER_MIN_INTERVAL must be an integer from 0 to 60000 ms; using 16 ms");
|
|
8
|
+
return 16;
|
|
9
|
+
}
|