buninu-browser 0.7.0 → 0.9.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/AGENTS.md +5 -0
- package/README.md +90 -1
- package/buninu-browser.js +1 -1
- package/frontends/wayland/README.md +35 -2
- package/frontends/wayland/client.js +8 -6
- package/frontends/wayland/clipboard.js +1 -1
- package/frontends/wayland/index.js +1 -1
- package/frontends/wayland/input.js +1 -1
- package/frontends/wayland/resource-io.js +6 -0
- package/frontends/wayland/transport-posix.js +120 -0
- package/frontends/wayland/transport-win32.js +9 -0
- package/frontends/wayland/transport.js +3 -120
- package/frontends/wayland/win32-resources.js +80 -0
- package/frontends/wayland/win32-wire.js +62 -0
- package/lib/audio/biquad.js +25 -0
- package/lib/audio/piano.js +6 -3
- package/lib/audio/playback-buffer.js +5 -5
- package/lib/audio/pulse-client.js +24 -5
- package/lib/audio/web-audio.js +570 -0
- package/lib/browser-dialog.js +116 -0
- package/lib/cdp-server.js +43 -11
- package/lib/file-picker.js +48 -0
- package/lib/headless-shell.js +34 -12
- package/lib/layout/text-layout.js +112 -19
- package/lib/media/mp4-frame-times.js +93 -0
- package/lib/paint/direct-canvas.ts +39 -0
- package/lib/paint/display-list.js +37 -12
- package/lib/paint/raster-readback.js +16 -7
- package/lib/renderer/animation-frames.js +10 -5
- package/lib/renderer/bindings.js +69 -16
- package/lib/renderer/canvas-2d.js +265 -15
- package/lib/renderer/canvas-round-rect.js +36 -0
- package/lib/renderer/css-animations.js +173 -0
- package/lib/renderer/dialog-client.js +30 -0
- package/lib/renderer/dialog-worker.js +35 -0
- package/lib/renderer/eval-environment.js +28 -0
- package/lib/renderer/file-input.js +49 -0
- package/lib/renderer/host.js +33 -4
- package/lib/renderer/html-drag.js +77 -0
- package/lib/renderer/image-loading.js +45 -2
- package/lib/renderer/main.js +4 -0
- package/lib/renderer/module-transform.js +5 -3
- package/lib/renderer/page-builtins.js +59 -0
- package/lib/renderer/page-realm.js +177 -40
- package/lib/renderer/page-renderer.js +397 -49
- package/lib/renderer/piano-input.js +3 -1
- package/lib/renderer/raster-surface.js +18 -0
- package/lib/renderer/script-rewrite.js +165 -9
- package/lib/renderer/video-presentation.js +5 -4
- package/lib/renderer/webgl/clip.js +24 -0
- package/lib/renderer/webgl/context.js +158 -0
- package/lib/renderer/webgl/cpu-fragment.js +211 -0
- package/lib/renderer/webgl/draw-cache.js +22 -0
- package/lib/renderer/webgl/pipeline.js +70 -0
- package/lib/renderer/webgl/shaders.js +58 -0
- package/lib/renderer/webgl/textures.js +43 -0
- package/lib/renderer/window-named-properties.js +46 -0
- package/lib/screenshot-cli.js +4 -7
- package/lib/style/animation-values.js +29 -0
- package/lib/style/computed-style.js +109 -13
- package/lib/style/transform.js +44 -0
- package/lib/window-size.js +10 -0
- package/package.json +1 -1
- package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
- package/patches/happy-dom/README.md +16 -0
- package/test/animation-frames.test.js +15 -0
- package/test/canvas-2d.test.js +252 -4
- package/test/canvas-frame-pacing.test.js +7 -0
- package/test/cdp-viewers.test.js +36 -0
- package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
- package/test/classic-games-patches/README.md +156 -0
- package/test/classic-games-scenarios.js +133 -0
- package/test/clipboard.test.js +16 -0
- package/test/computed-style.test.js +18 -0
- package/test/css-animation-lifecycle.test.js +28 -0
- package/test/css-animations.test.js +83 -0
- package/test/direct-canvas.test.js +39 -0
- package/test/dynamic-inline-handlers.test.js +25 -0
- package/test/eval-declarations.test.js +22 -0
- package/test/file-picker.test.js +26 -0
- package/test/fixtures/canvas-damage.js +23 -0
- package/test/fixtures/canvas-frame-pacing.js +35 -0
- package/test/fixtures/direct-canvas.js +96 -0
- package/test/fixtures/javascript-dialogs.html +8 -0
- package/test/fixtures/media-audio-lifetime.js +65 -0
- package/test/fixtures/renderer-memory.js +7 -5
- package/test/fixtures/video-black-replay.js +8 -6
- package/test/fixtures/video-slow-end.js +39 -0
- package/test/fixtures/web-audio-renderer.js +45 -0
- package/test/flex.test.js +26 -0
- package/test/grid.test.js +13 -0
- package/test/headless-shell.test.js +9 -0
- package/test/headless-window-size.test.js +28 -0
- package/test/html-rcdata.test.js +35 -0
- package/test/image-decode.test.js +18 -0
- package/test/javascript-dialog.test.js +83 -0
- package/test/manual-breakout-paddle.js +42 -0
- package/test/manual-breakout-static.js +35 -0
- package/test/manual-classic-games.js +41 -0
- package/test/manual-clumsy-bird-three-pipes.js +81 -0
- package/test/manual-clumsy-bird.js +52 -0
- package/test/manual-hextris.js +30 -0
- package/test/manual-numbered-music.js +24 -0
- package/test/manual-t-rex-runner.js +49 -0
- package/test/manual-threejs-scene.js +64 -0
- package/test/manual-webgl-benchmark.js +27 -0
- package/test/manual-webgl-cpu.html +26 -0
- package/test/media-audio-lifetime.test.js +8 -0
- package/test/mp4-frame-times.test.js +33 -0
- package/test/page-scripts.test.js +96 -1
- package/test/piano-input.test.js +15 -2
- package/test/piano.test.js +11 -2
- package/test/playback-buffer.test.js +11 -0
- package/test/pointer-drag.test.js +33 -0
- package/test/positioning.test.js +82 -0
- package/test/pulse-client.test.js +51 -2
- package/test/raster-readback.test.js +14 -0
- package/test/script-rewrite.test.js +61 -3
- package/test/video-slow-end.test.js +6 -0
- package/test/viewport-keyboard-overflow.test.js +25 -0
- package/test/wayland-win32-resources.test.js +47 -0
- package/test/wayland-win32-wire.test.js +59 -0
- package/test/web-audio-renderer.test.js +8 -0
- package/test/web-audio.test.js +264 -0
- package/test/webgl-clip.test.js +9 -0
- package/test/webgl-cpu-fragment.test.js +43 -0
- package/test/window-named-properties.test.js +41 -0
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
- package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
- package/vendor/land-renderer/LICENSE +21 -0
- package/vendor/land-renderer/README.md +14 -0
- package/vendor/land-renderer/direct-compositor.ts +443 -0
- package/vendor/land-renderer/frame.js +88 -0
- package/vendor/land-renderer/png-fast.js +85 -0
- package/vendor/land-renderer/raw-frames.js +66 -0
- package/vendor/land-renderer/render-types.ts +7 -0
- package/vendor/land-renderer/renderer.ts +134 -0
- package/vendor/land-renderer/snapshot.js +37 -0
- package/vendor/land-renderer/viewport.js +18 -0
- package/vendor/ses/dist/ses.mjs +1 -1
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { timeVideoDecoder } from "../media/mp4-frame-times.js";
|
|
2
|
+
import {createCSSAnimations} from './css-animations.js';
|
|
3
|
+
import { CanvasComposer } from "../paint/direct-canvas.ts";
|
|
4
|
+
import { makeRasterSurface } from "./raster-surface.js";
|
|
5
|
+
import {chooseInputFiles} from './file-input.js';
|
|
6
|
+
import {inverseTransformPoint,transformRect} from '../paint/display-list.js';
|
|
7
|
+
import { HTMLDrag, isDraggable } from './html-drag.js';
|
|
1
8
|
/**
|
|
2
9
|
* The page pipeline of one renderer: parse, style, layout, display list and
|
|
3
10
|
* CanvasKit raster, plus the page-side input helpers (scrolling, hit testing,
|
|
@@ -50,7 +57,7 @@ import { LayoutEngine } from "../layout/text-layout.js";
|
|
|
50
57
|
import { ScrollViewport } from "../layout/scroll-viewport.js";
|
|
51
58
|
import { scrollAlignmentDelta } from "./scroll-into-view.js";
|
|
52
59
|
import { elementAtPoint, elementBounds, elementBox, hitTest, interactiveRegions, renderNodeAtPoint } from "../input/hit-test.js";
|
|
53
|
-
import { beginImageLoad, finishImageLoad } from "./image-loading.js";
|
|
60
|
+
import { beginImageLoad, finishImageLoad, imageSourceChanged } from "./image-loading.js";
|
|
54
61
|
import { createImageResourceLoader, imageCrossOrigin, imageRequestKey } from "./image-resources.js";
|
|
55
62
|
import { createResourceElements } from "./resource-elements.js";
|
|
56
63
|
import { DocumentFonts } from "./font-loading.js";
|
|
@@ -252,6 +259,7 @@ function buildPageRenderer({
|
|
|
252
259
|
requestDownload = () => {},
|
|
253
260
|
reportError = () => {},
|
|
254
261
|
pageFetch = () => Promise.reject(new TypeError("network access is not available")),
|
|
262
|
+
dialog = (type) => type === "confirm" ? false : type === "prompt" ? null : undefined,
|
|
255
263
|
clipboardReadInput = () => clipboardRead(),
|
|
256
264
|
clipboardWriteInput = text => clipboardWrite(text),
|
|
257
265
|
clipboardRead = () => Promise.reject(Error("Clipboard unavailable")),
|
|
@@ -314,7 +322,19 @@ function buildPageRenderer({
|
|
|
314
322
|
}
|
|
315
323
|
const renderTreeBuilder = new RenderTreeBuilder();
|
|
316
324
|
let layoutEngine = new LayoutEngine({ measureText, fontMetrics });
|
|
317
|
-
const
|
|
325
|
+
const cssAnimations = createCSSAnimations({
|
|
326
|
+
invalidate(element) { markStyleDirty(element);page.dirty=true;scheduleFrame(true); },
|
|
327
|
+
dispatch(element,type,detail) {
|
|
328
|
+
if(!page.realm||element.ownerDocument!==page.document)return;
|
|
329
|
+
const event=new page.window.AnimationEvent(type,{bubbles:true,...detail});
|
|
330
|
+
page.realm.dispatch(element,event);
|
|
331
|
+
// Legacy event names are aliases for the same CSS animation timeline.
|
|
332
|
+
if(type!=='animationcancel') {
|
|
333
|
+
const alias=new page.window.AnimationEvent({animationstart:'webkitAnimationStart',animationiteration:'webkitAnimationIteration',animationend:'webkitAnimationEnd'}[type],{bubbles:true,...detail});page.realm.dispatch(element,alias);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
const styleEngine = new StyleEngine({ measureText, deferHidden: true, sampleStyle:(element,style)=>cssAnimations.sample(element,style) });
|
|
318
338
|
// Incremental restyle: what may have changed style since the last
|
|
319
339
|
// computation. DOM mutations come from a MutationObserver; state that is
|
|
320
340
|
// not in the DOM tree (checked, value, focus) is marked by the code that
|
|
@@ -357,7 +377,9 @@ function buildPageRenderer({
|
|
|
357
377
|
// The frame keyboard input goes to, after a click into it.
|
|
358
378
|
let focusedFrame = null;
|
|
359
379
|
let pointerButtons = 0, suppressPointerMouse = false;
|
|
380
|
+
let spaceDisclosureActivation = null;
|
|
360
381
|
const pointerPresses = new Map();
|
|
382
|
+
let htmlDrag = null;
|
|
361
383
|
let selectionDrag = null;
|
|
362
384
|
let selectionVersion = 0;
|
|
363
385
|
const viewport = new ScrollViewport(800, 600);
|
|
@@ -387,6 +409,7 @@ function buildPageRenderer({
|
|
|
387
409
|
decodedImages: [],
|
|
388
410
|
imageElements: new Set(),
|
|
389
411
|
imagesLoaded: null,
|
|
412
|
+
scriptImages: new Set(),
|
|
390
413
|
// CSS background images by absolute URL: { resource, loaded } (see queueBackgroundImages).
|
|
391
414
|
backgrounds: new Map(),
|
|
392
415
|
// Scroll offsets of scroll containers, by element (see setElementScroll).
|
|
@@ -618,6 +641,7 @@ function buildPageRenderer({
|
|
|
618
641
|
const captures = new FrameCaptureCache();
|
|
619
642
|
let prepaintTimer = null;
|
|
620
643
|
let frameTimer = null;
|
|
644
|
+
let lastAnimationPaint = -Infinity;
|
|
621
645
|
let lastRebuildMs = 0;
|
|
622
646
|
|
|
623
647
|
// What the latest tile was rasterized from. A rebuild that produces the
|
|
@@ -632,6 +656,10 @@ function buildPageRenderer({
|
|
|
632
656
|
let rasterSurfaceHeight = 0;
|
|
633
657
|
let rasterRegion = null;
|
|
634
658
|
const rasterReadback = new RasterReadback(CanvasKit, { shared: Boolean(onFrame), allocate: allocateFramePixels });
|
|
659
|
+
const canvasComposer = new CanvasComposer();
|
|
660
|
+
let canvasLayers = null;
|
|
661
|
+
let canvasPixels = new WeakMap();
|
|
662
|
+
let clippedCanvasPixels = new WeakMap();
|
|
635
663
|
// A scroll or unrelated DOM update does not change text shaping. Keep a
|
|
636
664
|
// bounded set of visible paragraphs keyed by their complete shaping input.
|
|
637
665
|
// Both limits matter: a page may contain many unique or very long strings.
|
|
@@ -744,7 +772,108 @@ function buildPageRenderer({
|
|
|
744
772
|
schedulePrepaint();
|
|
745
773
|
}
|
|
746
774
|
|
|
747
|
-
/**
|
|
775
|
+
/** Adapt one or more ordinary Canvas display items to Land surfaces. */
|
|
776
|
+
function composeCanvasLayers(region,width,height) {
|
|
777
|
+
const list=page.displayList,background=backgroundBytes(page.backgroundColor);
|
|
778
|
+
const selection=page.window?.getSelection();
|
|
779
|
+
if(background[3]!==255||page.hintsVisible||(selection&&!selection.isCollapsed)
|
|
780
|
+
||framesVersion||list.items.some(item=>item.op==='drawVideo'))return null;
|
|
781
|
+
const groups=[],canvases=[];let pending=[];
|
|
782
|
+
for(const item of list.items){
|
|
783
|
+
if(item.op!=='drawImage'||page.renderTree.nodesById.get(item.nodeId)?.domNode?.tagName!=='CANVAS'){
|
|
784
|
+
pending.push(item);continue;
|
|
785
|
+
}
|
|
786
|
+
// Complex CSS effects still use the ordinary renderer until their layer
|
|
787
|
+
// rasterization can preserve those effects independently of composition.
|
|
788
|
+
if(item.fixed||item.transform
|
|
789
|
+
||item.opacity<1||item.radii?.x||item.radii?.y)return null;
|
|
790
|
+
const resource=list.images[item.image];
|
|
791
|
+
if(!resource?.image)return null;
|
|
792
|
+
groups.push(pending);pending=[];canvases.push({item,resource});
|
|
793
|
+
}
|
|
794
|
+
if(!canvases.length)return null;
|
|
795
|
+
groups.push(pending);
|
|
796
|
+
// Ignore animated Canvas resources in the static layers' cache key.
|
|
797
|
+
const indices=new Set(canvases.map(({item})=>item.image));
|
|
798
|
+
const images=list.images.map((resource,index)=>indices.has(index)?null:resource);
|
|
799
|
+
const key={region,scale:page.deviceScaleFactor,background:page.backgroundColor,
|
|
800
|
+
scrollX:viewport.x,scrollY:viewport.y,groups};
|
|
801
|
+
if(!canvasLayers||!Bun.deepEquals(canvasLayers.key,key)
|
|
802
|
+
||canvasLayers.images.length!==images.length||images.some((image,index)=>image!==canvasLayers.images[index])){
|
|
803
|
+
const layers=groups.map(items=>{
|
|
804
|
+
if(!items.length)return null;
|
|
805
|
+
const surface=makeRasterSurface(CanvasKit,width,height);
|
|
806
|
+
if(!surface)throw Error('Unable to rasterize Canvas DOM layer');
|
|
807
|
+
try{
|
|
808
|
+
const canvas=surface.getCanvas();canvas.clear(CanvasKit.TRANSPARENT);
|
|
809
|
+
paintDisplayList(canvas,{...list,images:list.images,items},region,page.deviceScaleFactor);
|
|
810
|
+
surface.flush();const image=surface.makeImageSnapshot();
|
|
811
|
+
try{
|
|
812
|
+
const pixels=rasterReadback.read(image,width,height,{premultiplied:true,allocate:bytes=>new Uint8Array(bytes)});
|
|
813
|
+
// Cache just the painted bounds, not a viewport-sized transparent
|
|
814
|
+
// surface for a tiny marker. Land then visits only this small layer.
|
|
815
|
+
const words=new Uint32Array(pixels.buffer,pixels.byteOffset,pixels.byteLength/4);
|
|
816
|
+
let left=width,top=height,right=0,bottom=0;
|
|
817
|
+
for(let y=0,at=0;y<height;y++)for(let x=0;x<width;x++,at++)if(words[at]>>>24){
|
|
818
|
+
if(x<left)left=x;if(y<top)top=y;if(x>=right)right=x+1;if(y>=bottom)bottom=y+1;
|
|
819
|
+
}
|
|
820
|
+
if(right<=left||bottom<=top)return null;
|
|
821
|
+
const w=right-left,h=bottom-top;
|
|
822
|
+
const cropped=cropViewport({pixels,pixelWidth:width,pixelHeight:height,x:0,y:0,width,height,scale:1},left,top,w,h);
|
|
823
|
+
return {pixels:cropped.pixels,width:w,height:h,x:left,y:top,drawWidth:w,drawHeight:h};
|
|
824
|
+
}finally{image.delete();}
|
|
825
|
+
}finally{surface.dispose();}
|
|
826
|
+
});
|
|
827
|
+
canvasLayers={key,images,layers};
|
|
828
|
+
trace?.(`${page.url}: direct canvas DOM layers rebuilt`);
|
|
829
|
+
}
|
|
830
|
+
const layers=[];
|
|
831
|
+
for(let i=0;i<groups.length;i++){
|
|
832
|
+
const layer=canvasLayers.layers[i];
|
|
833
|
+
if(layer)layers.push(layer);
|
|
834
|
+
if(i===canvases.length)continue;
|
|
835
|
+
const {item,resource}=canvases[i];
|
|
836
|
+
const r=item.rect,scale=page.deviceScaleFactor;
|
|
837
|
+
const x=Math.round((r.x-region.x)*scale),y=Math.round((r.y-region.y)*scale);
|
|
838
|
+
const drawWidth=Math.round(r.width*scale),drawHeight=Math.round(r.height*scale);
|
|
839
|
+
if(drawWidth<=0||drawHeight<=0)continue;
|
|
840
|
+
if(item.clipRect||item.roundedClips?.length){
|
|
841
|
+
// Preserve CSS clipping with Skia on this Canvas layer alone. The DOM
|
|
842
|
+
// layers remain cached and Land still performs final composition.
|
|
843
|
+
// Align the local raster to the global device grid, including at DPR
|
|
844
|
+
// fractions; rounding the clip in a different origin changes its edge.
|
|
845
|
+
const view={x:region.x+x/scale,y:region.y+y/scale,width:drawWidth/scale,height:drawHeight/scale};
|
|
846
|
+
const key={view,scale,clipRect:item.clipRect,roundedClips:item.roundedClips};
|
|
847
|
+
let cached=clippedCanvasPixels.get(resource);
|
|
848
|
+
if(!cached||!Bun.deepEquals(cached.key,key)){
|
|
849
|
+
const surface=makeRasterSurface(CanvasKit,drawWidth,drawHeight);
|
|
850
|
+
if(!surface) return null;
|
|
851
|
+
try{
|
|
852
|
+
const canvas=surface.getCanvas();canvas.clear(CanvasKit.TRANSPARENT);
|
|
853
|
+
paintDisplayList(canvas,{...list,images:list.images,items:[item]},view,scale);
|
|
854
|
+
surface.flush();
|
|
855
|
+
const image=surface.makeImageSnapshot();
|
|
856
|
+
try{cached={key,pixels:rasterReadback.read(image,drawWidth,drawHeight,
|
|
857
|
+
{premultiplied:true,allocate:bytes=>new Uint8Array(bytes)})};}
|
|
858
|
+
finally{image.delete();}
|
|
859
|
+
}finally{surface.dispose();}
|
|
860
|
+
clippedCanvasPixels.set(resource,cached);
|
|
861
|
+
}
|
|
862
|
+
layers.push({pixels:cached.pixels,width:drawWidth,height:drawHeight,x,y,drawWidth,drawHeight});
|
|
863
|
+
}else{
|
|
864
|
+
let source=canvasPixels.get(resource);
|
|
865
|
+
if(!source){source=rasterReadback.read(resource.image,resource.width,resource.height,
|
|
866
|
+
{premultiplied:true,allocate:bytes=>new Uint8Array(bytes)});canvasPixels.set(resource,source);}
|
|
867
|
+
layers.push({pixels:source,width:resource.width,height:resource.height,x,y,drawWidth,drawHeight});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const composed=canvasComposer.compose(width,height,background,layers);
|
|
871
|
+
const pixels=allocateFramePixels?.(composed.length)
|
|
872
|
+
??new Uint8Array(onFrame?new SharedArrayBuffer(composed.length):new ArrayBuffer(composed.length));
|
|
873
|
+
pixels.set(composed);return pixels;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** Conservative canvas and plain-text damage, in document CSS pixels. */
|
|
748
877
|
function canvasDamage(region) {
|
|
749
878
|
const old = rasterized.list, next = page.displayList;
|
|
750
879
|
// Reuse pixels only when geometry and every non-canvas input are stable.
|
|
@@ -754,9 +883,31 @@ function buildPageRenderer({
|
|
|
754
883
|
|| rasterized.overlay !== overlayKey() || rasterized.frames !== framesVersion
|
|
755
884
|
|| rasterized.scrollX !== viewport.x || rasterized.scrollY !== viewport.y
|
|
756
885
|
|| !old || !next || old.images.length !== next.images.length
|
|
757
|
-
||
|
|
886
|
+
|| old.items.length !== next.items.length
|
|
758
887
|
|| next.items.some(item => item.op === 'drawVideo')) return null;
|
|
759
888
|
let damage = null;
|
|
889
|
+
const include = (r, pad = 1) => {
|
|
890
|
+
if (!r || ![r.x,r.y,r.width,r.height,pad].every(Number.isFinite) || r.width < 0 || r.height < 0) return false;
|
|
891
|
+
const left=r.x-pad,top=r.y-pad,right=r.x+r.width+pad,bottom=r.y+r.height+pad;
|
|
892
|
+
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)}
|
|
893
|
+
:{x:left,y:top,right,bottom};
|
|
894
|
+
return true;
|
|
895
|
+
};
|
|
896
|
+
for(let i=0;i<next.items.length;i++){
|
|
897
|
+
const before=old.items[i],after=next.items[i];
|
|
898
|
+
if(Bun.deepEquals(before,after))continue;
|
|
899
|
+
// Only the text and its measured width may change. Keep line position,
|
|
900
|
+
// font, clips and every effect identical; reflow/other changes repaint fully.
|
|
901
|
+
if(before.op!=='drawText'||after.op!=='drawText'||before.fixed||after.fixed
|
|
902
|
+
||before.transform||after.transform||before.gradient||after.gradient)return null;
|
|
903
|
+
const {text:bt,textWidth:bw,bounds:bb,nodeId:bi,...a}=before;
|
|
904
|
+
const {text:at,textWidth:aw,bounds:ab,nodeId:ai,...b}=after;
|
|
905
|
+
if(!Bun.deepEquals(a,b))return null;
|
|
906
|
+
// Include overhangs and antialiasing outside advance widths, as well as
|
|
907
|
+
// the previous glyphs when the replacement string becomes shorter.
|
|
908
|
+
const pad=Math.max(2,before.font.size*2);
|
|
909
|
+
if(!include(bb,pad)||!include(ab,pad))return null;
|
|
910
|
+
}
|
|
760
911
|
for (let i = 0; i < next.images.length; i++) {
|
|
761
912
|
if (old.images[i] === next.images[i]) continue;
|
|
762
913
|
const uses = next.items.filter(item => item.image === i);
|
|
@@ -785,6 +936,18 @@ function buildPageRenderer({
|
|
|
785
936
|
const scale = page.deviceScaleFactor;
|
|
786
937
|
const pixelWidth = Math.max(1, Math.round(region.width * scale));
|
|
787
938
|
const pixelHeight = Math.max(1, Math.round(region.height * scale));
|
|
939
|
+
// Canvas snapshots are layers in Land's unmodified TS compositor. Static
|
|
940
|
+
// DOM below/above them is rasterized only when its own inputs change.
|
|
941
|
+
const direct = composeCanvasLayers(region,pixelWidth,pixelHeight);
|
|
942
|
+
if(direct){
|
|
943
|
+
rasterRegion=null;
|
|
944
|
+
page.needsPaint=false;
|
|
945
|
+
rasterized={list:page.displayList,overlay:overlayKey(),background:page.backgroundColor,scale,
|
|
946
|
+
scrollX:viewport.x,scrollY:viewport.y,frames:framesVersion};
|
|
947
|
+
trace?.(`${page.url}: direct canvas composition ${pixelWidth}x${pixelHeight}`);
|
|
948
|
+
return {pixels:direct,pixelWidth,pixelHeight,x:region.x,y:region.y,width:region.width,height:region.height,
|
|
949
|
+
scale,background:backgroundBytes(page.backgroundColor),videoLayer:null};
|
|
950
|
+
}
|
|
788
951
|
// The independent compositor already reconstructs an opaque video from
|
|
789
952
|
// black/white masks. Reuse that path for the ordinary tile too, avoiding
|
|
790
953
|
// a large Skia image upscale and a second black pass on each UI repaint.
|
|
@@ -794,7 +957,7 @@ function buildPageRenderer({
|
|
|
794
957
|
if (!rasterSurface || rasterSurfaceWidth !== pixelWidth || rasterSurfaceHeight !== pixelHeight) {
|
|
795
958
|
// MakeSurface owns a malloc-backed pixel buffer; delete() alone leaks it.
|
|
796
959
|
rasterSurface?.dispose();
|
|
797
|
-
rasterSurface = CanvasKit
|
|
960
|
+
rasterSurface = makeRasterSurface(CanvasKit,pixelWidth,pixelHeight);
|
|
798
961
|
rasterSurfaceWidth = pixelWidth;
|
|
799
962
|
rasterSurfaceHeight = pixelHeight;
|
|
800
963
|
}
|
|
@@ -817,7 +980,14 @@ function buildPageRenderer({
|
|
|
817
980
|
surface.flush();
|
|
818
981
|
const image = surface.makeImageSnapshot();
|
|
819
982
|
let pixels;
|
|
820
|
-
try {
|
|
983
|
+
try {
|
|
984
|
+
const left=damage?Math.max(0,Math.floor((damage.x-region.x)*scale)):0;
|
|
985
|
+
const top=damage?Math.max(0,Math.floor((damage.y-region.y)*scale)):0;
|
|
986
|
+
const right=damage?Math.min(pixelWidth,Math.ceil((damage.right-region.x)*scale)):pixelWidth;
|
|
987
|
+
const bottom=damage?Math.min(pixelHeight,Math.ceil((damage.bottom-region.y)*scale)):pixelHeight;
|
|
988
|
+
pixels = rasterReadback.read(image, pixelWidth, pixelHeight, damage && lastTile
|
|
989
|
+
? {previous:lastTile.pixels,region:{x:left,y:top,width:right-left,height:bottom-top}} : undefined);
|
|
990
|
+
}
|
|
821
991
|
finally { image.delete(); }
|
|
822
992
|
const videoLayer = makeVideoLayer(canvas, surface, region, scale, pixelWidth, pixelHeight, pixels, blackPlan?.resource);
|
|
823
993
|
if (blackPlan && !videoLayer) {
|
|
@@ -920,7 +1090,7 @@ function buildPageRenderer({
|
|
|
920
1090
|
}
|
|
921
1091
|
|
|
922
1092
|
/** A rendering opportunity for DOM changes nobody has asked to see yet. */
|
|
923
|
-
function scheduleFrame() {
|
|
1093
|
+
function scheduleFrame(animation = false) {
|
|
924
1094
|
if (disposedRenderer || !onFrame || frameTimer || page.loading) return;
|
|
925
1095
|
frameTimer = hostSetTimeout(() => {
|
|
926
1096
|
frameTimer = null;
|
|
@@ -928,7 +1098,11 @@ function buildPageRenderer({
|
|
|
928
1098
|
// Coalesce ordinary mutations for the configured minimum interval.
|
|
929
1099
|
// Expensive style/layout rebuilds retain a longer idle window so related script mutations can settle
|
|
930
1100
|
// instead of continuously rebuilding a large document.
|
|
931
|
-
|
|
1101
|
+
// A previous expensive reflow must not permanently throttle Canvas-only
|
|
1102
|
+
// animation, which does not need another layout pass.
|
|
1103
|
+
// CSS animation samples are already paced and must not inherit the
|
|
1104
|
+
// mutation-settling delay, which skips most of a short drop/bounce.
|
|
1105
|
+
}, Math.max(renderMinInterval, !animation && page.dirty ? lastRebuildMs * 2 : 0));
|
|
932
1106
|
}
|
|
933
1107
|
|
|
934
1108
|
/** The viewport as an encoded image (base64), cut from the latest tile. */
|
|
@@ -988,8 +1162,8 @@ function buildPageRenderer({
|
|
|
988
1162
|
}
|
|
989
1163
|
// A scale in document space, as a device-space matrix around the item's device coordinates.
|
|
990
1164
|
if (item.transform) {
|
|
991
|
-
const { a, d, e, f } = item.transform;
|
|
992
|
-
canvas.concat([a,
|
|
1165
|
+
const { a, b=0, c=0, d, e, f } = item.transform;
|
|
1166
|
+
canvas.concat([a, c, ((a - 1) * view.x + c*view.y + e) * scale, b, d, (b*view.x + (d - 1) * view.y + f) * scale, 0, 0, 1]);
|
|
993
1167
|
}
|
|
994
1168
|
if (item.opacity < 1) {
|
|
995
1169
|
const layer = new CanvasKit.Paint();
|
|
@@ -1718,7 +1892,9 @@ function buildPageRenderer({
|
|
|
1718
1892
|
if (page.document) loadImageResource.cancelDocument(page.document);
|
|
1719
1893
|
resourceElements?.dispose();
|
|
1720
1894
|
resetInputCaret();
|
|
1895
|
+
lastAnimationPaint=-Infinity;
|
|
1721
1896
|
rasterReadback.dispose();
|
|
1897
|
+
canvasComposer.close();canvasLayers=null;canvasPixels=new WeakMap();clippedCanvasPixels=new WeakMap();
|
|
1722
1898
|
clearPaintParagraphs();
|
|
1723
1899
|
resetDocumentFonts();
|
|
1724
1900
|
disposeFrames();
|
|
@@ -1727,13 +1903,14 @@ function buildPageRenderer({
|
|
|
1727
1903
|
// qml handle begin: dispose application resources with their document
|
|
1728
1904
|
page.document?.__qmlHost?.dispose();
|
|
1729
1905
|
// qml handle end
|
|
1906
|
+
cssAnimations.dispose();
|
|
1730
1907
|
page.realm?.dispose();
|
|
1731
1908
|
page.realm = null;
|
|
1732
1909
|
page.window?.happyDOM?.close?.();
|
|
1733
1910
|
for (const image of page.decodedImages) image.delete();
|
|
1734
1911
|
disposeBackgrounds();
|
|
1735
1912
|
page.window = parsed.window;
|
|
1736
|
-
pointerButtons = 0; suppressPointerMouse = false; pointerPresses.clear(); selectionDrag=null;
|
|
1913
|
+
pointerButtons = 0; suppressPointerMouse = false; pointerPresses.clear(); selectionDrag=null; htmlDrag=null;spaceDisclosureActivation=null;
|
|
1737
1914
|
page.document = parsed.document;
|
|
1738
1915
|
resourceElements = createResourceElements(parsed.document);
|
|
1739
1916
|
computedSheets = null;
|
|
@@ -1750,11 +1927,13 @@ function buildPageRenderer({
|
|
|
1750
1927
|
page.styleSheets = styleSheets;
|
|
1751
1928
|
page.linkSheets = linkSheets;
|
|
1752
1929
|
page.resources = imageResources.map;
|
|
1930
|
+
for(const {element,src,resource} of imageResources.entries)finishImageLoad(element,src,resource);
|
|
1753
1931
|
initializeVideos(parsed.document, page.resources);
|
|
1754
1932
|
page.decodedImages = imageResources.decoded;
|
|
1755
1933
|
page.imageElements = new Set(imageResources.entries.map(entry => entry.element));
|
|
1756
1934
|
page.resourcePaintDirty = false;
|
|
1757
1935
|
page.imagesLoaded = null;
|
|
1936
|
+
page.scriptImages.clear();
|
|
1758
1937
|
imageRequests = new WeakMap();
|
|
1759
1938
|
page.elementScroll = new Map();
|
|
1760
1939
|
const timing = styleAndLayout();
|
|
@@ -1808,6 +1987,7 @@ function buildPageRenderer({
|
|
|
1808
1987
|
lastStyleParts = `changes ${Math.round(taken - started)}, no invalidation`;
|
|
1809
1988
|
return { style: Math.round(taken - started), layout: 0, skipped: true, geometryUnchanged: true };
|
|
1810
1989
|
}
|
|
1990
|
+
cssAnimations.setSheets(sheets,environment);
|
|
1811
1991
|
styleEngine.compute(page.document, sheets, environment, fullStyle ? null : changes?.roots ?? null, changes?.self ?? null);
|
|
1812
1992
|
computedSheets = sheets;
|
|
1813
1993
|
computedEnvironment = environmentKey;
|
|
@@ -1949,6 +2129,7 @@ function buildPageRenderer({
|
|
|
1949
2129
|
function loadDocument(document) {
|
|
1950
2130
|
if (disposedRenderer) return Promise.reject(new Error("Renderer has been disposed"));
|
|
1951
2131
|
const generation = ++loadGeneration;
|
|
2132
|
+
clipboardActivationUntil = 0;
|
|
1952
2133
|
let commit;
|
|
1953
2134
|
const committed = new Promise((resolve, reject) => {
|
|
1954
2135
|
commit = { resolve: () => resolve(generation), reject };
|
|
@@ -1999,9 +2180,9 @@ function buildPageRenderer({
|
|
|
1999
2180
|
}
|
|
2000
2181
|
if (url === "about:blank") {
|
|
2001
2182
|
replaceDocument("", url);
|
|
2002
|
-
//
|
|
2003
|
-
//
|
|
2004
|
-
if (
|
|
2183
|
+
// Blank documents also need a realm for javascript: URLs, CDP evaluation
|
|
2184
|
+
// and parent access through iframe.contentWindow/contentDocument.
|
|
2185
|
+
if (page.scripting !== "off") {
|
|
2005
2186
|
page.realm = createPageRealm({ window: page.window, document: page.document, mode: page.scripting, hooks: realmHooks(generation) });
|
|
2006
2187
|
render();
|
|
2007
2188
|
commit.resolve();
|
|
@@ -2379,6 +2560,30 @@ function buildPageRenderer({
|
|
|
2379
2560
|
return to>from?{from,to}:null;
|
|
2380
2561
|
}
|
|
2381
2562
|
|
|
2563
|
+
let clipboardActivationUntil = 0;
|
|
2564
|
+
let executingCopy = false;
|
|
2565
|
+
function execCommand(command) {
|
|
2566
|
+
if (command !== 'copy' || executingCopy || Date.now() > clipboardActivationUntil) return false;
|
|
2567
|
+
executingCopy = true;
|
|
2568
|
+
try {
|
|
2569
|
+
const element = page.document.activeElement;
|
|
2570
|
+
const target = element ?? page.document.body;
|
|
2571
|
+
const event = new page.window.Event('copy', {bubbles:true,cancelable:true});
|
|
2572
|
+
target?.dispatchEvent(event);
|
|
2573
|
+
if (event.defaultPrevented) return true;
|
|
2574
|
+
// Capture synchronously: the caller can remove its temporary textarea
|
|
2575
|
+
// immediately after execCommand returns. Reuse the established frontend
|
|
2576
|
+
// clipboard transport, which retains the host's user-action checks.
|
|
2577
|
+
const text = isTextInput(element) && element.type !== 'password'
|
|
2578
|
+
? String(element.value).slice(element.selectionStart, element.selectionEnd)
|
|
2579
|
+
: page.window.getSelection().toString();
|
|
2580
|
+
if (!text) return false;
|
|
2581
|
+
clipboardActivationUntil = 0;
|
|
2582
|
+
Promise.resolve(clipboardWriteInput(text)).catch(reportError);
|
|
2583
|
+
return true;
|
|
2584
|
+
} finally { executingCopy = false; }
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2382
2587
|
function clipboardForInput(element,operation,backend={read:clipboardReadInput,write:clipboardWriteInput}) {
|
|
2383
2588
|
return inputClipboard(element,operation,{...backend,replace:(text,type)=>{
|
|
2384
2589
|
if(page.document.activeElement!==element)return;
|
|
@@ -2466,6 +2671,7 @@ function buildPageRenderer({
|
|
|
2466
2671
|
|
|
2467
2672
|
function realmHooks(generation) {
|
|
2468
2673
|
return {
|
|
2674
|
+
decodeAudio: videoDecoder ? bytes => videoDecoder.decodeAudio(bytes) : null,
|
|
2469
2675
|
frames: {
|
|
2470
2676
|
self: ownWindow,
|
|
2471
2677
|
parent: ownWindow.parent(),
|
|
@@ -2481,6 +2687,10 @@ function buildPageRenderer({
|
|
|
2481
2687
|
scheduleFrame();
|
|
2482
2688
|
},
|
|
2483
2689
|
touch(node = null, stateFeatures = []) {
|
|
2690
|
+
if(node?.tagName==='IMG')imageSourceChanged(node);
|
|
2691
|
+
// HTML images load even when scripts keep them outside the DOM (e.g.
|
|
2692
|
+
// canvas and WebGL textures). They remain document-owned resources.
|
|
2693
|
+
if(node?.tagName==='IMG'&&!node.isConnected&&node.getAttribute('src'))page.scriptImages.add(node);
|
|
2484
2694
|
if (stateFeatures.length) markStyleDirty(node, stateFeatures);
|
|
2485
2695
|
page.dirty = true;
|
|
2486
2696
|
scheduleFrame();
|
|
@@ -2526,6 +2736,7 @@ function buildPageRenderer({
|
|
|
2526
2736
|
return renderCanvasText({ ...request, style,
|
|
2527
2737
|
settings: { ...request.settings, direction: style.direction } });
|
|
2528
2738
|
},
|
|
2739
|
+
audio,
|
|
2529
2740
|
mediaState,
|
|
2530
2741
|
playMedia,
|
|
2531
2742
|
pauseMedia,
|
|
@@ -2553,10 +2764,32 @@ function buildPageRenderer({
|
|
|
2553
2764
|
whenImagesLoaded: () => Promise.all([page.imagesLoaded, documentFonts.ready(), ...[...page.backgrounds.values()].map((entry) => entry.loaded)]),
|
|
2554
2765
|
selectionChanged(){selectionVersion++;page.needsPaint=true;scheduleFrame();},
|
|
2555
2766
|
afterTask() {},
|
|
2767
|
+
afterAnimationFrame() {
|
|
2768
|
+
if(disposedRenderer||page.loading||!onFrame||page.dirty||!page.resourcePaintDirty||!page.needsPaint)return;
|
|
2769
|
+
const now=hostPerformance.now();
|
|
2770
|
+
if(now-lastAnimationPaint<renderMinInterval)return;
|
|
2771
|
+
lastAnimationPaint=now;
|
|
2772
|
+
hostClearTimeout(frameTimer);frameTimer=null;
|
|
2773
|
+
refresh();
|
|
2774
|
+
},
|
|
2775
|
+
pickFile(element){void chooseInputFiles(element,dialog?.pickFiles,page.window).catch(error=>console.error(error));},
|
|
2776
|
+
layoutBounds(element){if(page.dirty)rebuildRenderTree("script read layout geometry");const box=elementBox(page.renderTree,page.layout,element)?.box;return box?{left:box.x,top:box.y,width:box.width,height:box.height}:null;},
|
|
2556
2777
|
boundsOf(element) {
|
|
2557
2778
|
if (page.dirty) rebuildRenderTree("script read geometry");
|
|
2558
2779
|
return viewportBounds(element);
|
|
2559
2780
|
},
|
|
2781
|
+
mouseOffset(target, x, y) {
|
|
2782
|
+
if (page.dirty) rebuildRenderTree("script read mouse offset");
|
|
2783
|
+
const element = target?.nodeType === 1 ? target : target?.parentElement;
|
|
2784
|
+
const geometry=element && elementBox(page.renderTree,page.layout,element);
|
|
2785
|
+
const hit=geometry && page.displayList?.hits.find(h=>h.nodeId===geometry.node.id && h.localRect);
|
|
2786
|
+
const rect = element && viewportBounds(element);
|
|
2787
|
+
if (!rect) return null;
|
|
2788
|
+
if(hit?.transform){const p=inverseTransformPoint(hit.transform,x+(hit.fixed?0:viewport.x),y+(hit.fixed?0:viewport.y));x=p.x;y=p.y;rect.left=hit.localRect.x;rect.top=hit.localRect.y;}
|
|
2789
|
+
// Offset coordinates are in the target's untransformed padding box.
|
|
2790
|
+
const borders = styleEngine.get(element)?.borderWidths ?? [0, 0, 0, 0];
|
|
2791
|
+
return { x: x - rect.left - borders[3], y: y - rect.top - borders[0] };
|
|
2792
|
+
},
|
|
2560
2793
|
viewport: () => ({
|
|
2561
2794
|
width: page.width,
|
|
2562
2795
|
height: page.height,
|
|
@@ -2645,6 +2878,9 @@ function buildPageRenderer({
|
|
|
2645
2878
|
setDocumentCookie(value) {
|
|
2646
2879
|
documentCookies.set(cookieUrl(), value);
|
|
2647
2880
|
},
|
|
2881
|
+
dialog,
|
|
2882
|
+
execCommand,
|
|
2883
|
+
copyEnabled: () => Date.now() <= clipboardActivationUntil,
|
|
2648
2884
|
clipboardRead,
|
|
2649
2885
|
clipboardWrite,
|
|
2650
2886
|
webSockets,
|
|
@@ -2712,7 +2948,10 @@ function buildPageRenderer({
|
|
|
2712
2948
|
|
|
2713
2949
|
/** The border box of an element's layout box in viewport coordinates. */
|
|
2714
2950
|
function viewportBounds(element) {
|
|
2715
|
-
const
|
|
2951
|
+
const geometry = elementBox(page.renderTree, page.layout, element);
|
|
2952
|
+
const box = geometry?.box;
|
|
2953
|
+
const hit=geometry && page.displayList?.hits.find(h=>h.nodeId===geometry.node.id && h.localRect);
|
|
2954
|
+
if(hit?.transform){const r=transformRect(hit.transform,hit.localRect);return {left:r.x-(hit.fixed?0:viewport.x),top:r.y-(hit.fixed?0:viewport.y),width:r.width,height:r.height};}
|
|
2716
2955
|
const content = box ? null : elementBounds(page.renderTree, page.layout, element);
|
|
2717
2956
|
if (!box && !content) return null;
|
|
2718
2957
|
let fixed = false;
|
|
@@ -2997,8 +3236,30 @@ function buildPageRenderer({
|
|
|
2997
3236
|
return { x: viewport.x, y: viewport.y };
|
|
2998
3237
|
},
|
|
2999
3238
|
press(key, options = {}) {
|
|
3239
|
+
clipboardActivationUntil = Date.now() + 5000;
|
|
3240
|
+
if(key==='Escape' && htmlDrag?.active){htmlDrag.end(undefined,undefined,options,true);htmlDrag=null;pointerButtons=0;pointerPresses.clear();selectionDrag=null;suppressPointerMouse=false;refresh();return {x:viewport.x,y:viewport.y};}
|
|
3000
3241
|
if (focusedFrame?.renderer) return focusedFrame.renderer.press(key, options);
|
|
3001
|
-
|
|
3242
|
+
// A CDP key-up is an independent task, not another synthetic key press.
|
|
3243
|
+
if (options.eventType === "keyup") {
|
|
3244
|
+
const target=page.document?.activeElement??page.document?.body;
|
|
3245
|
+
if(target&&page.realm){
|
|
3246
|
+
const modifiers=Number(options.modifiers)||0;
|
|
3247
|
+
const prevented=page.realm.dispatch(target,new page.window.KeyboardEvent("keyup",{
|
|
3248
|
+
...keyboardInput(key,options),bubbles:true,cancelable:true,
|
|
3249
|
+
altKey:!!(modifiers&1),ctrlKey:!!(modifiers&2),metaKey:!!(modifiers&4),shiftKey:!!(modifiers&8),
|
|
3250
|
+
}));
|
|
3251
|
+
if(key===' '){
|
|
3252
|
+
if(spaceDisclosureActivation?.deref()===target&&!prevented)activateDisclosure(target);
|
|
3253
|
+
spaceDisclosureActivation=null;
|
|
3254
|
+
}
|
|
3255
|
+
if(page.dirty)refresh();
|
|
3256
|
+
else if(page.needsPaint)scheduleFrame();
|
|
3257
|
+
}
|
|
3258
|
+
return {x:viewport.x,y:viewport.y};
|
|
3259
|
+
}
|
|
3260
|
+
// A held key's arrival must not force a second raster pass over an
|
|
3261
|
+
// otherwise current Canvas-only page before delivering the event.
|
|
3262
|
+
if(options.eventType!=="keydown"||page.dirty)refresh();
|
|
3002
3263
|
const pianoInput = page.document?.activeElement;
|
|
3003
3264
|
if (handlePianoInput(pianoInput, { ...options, key }, audio ? playPiano : null)) {
|
|
3004
3265
|
return { x: viewport.x, y: viewport.y };
|
|
@@ -3036,10 +3297,23 @@ function buildPageRenderer({
|
|
|
3036
3297
|
}
|
|
3037
3298
|
else if (String(key).length === 1) editFocusedText(String(key));
|
|
3038
3299
|
}
|
|
3039
|
-
|
|
3040
|
-
|
|
3300
|
+
if(options.eventType==='keydown'){
|
|
3301
|
+
if(key===' ')spaceDisclosureActivation=activated?new WeakRef(target):null;
|
|
3302
|
+
}else{
|
|
3303
|
+
const upPrevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keyup", init));
|
|
3304
|
+
if (activated && key === " " && !upPrevented) activateDisclosure(target);
|
|
3305
|
+
}
|
|
3306
|
+
if(options.eventType==='keydown'&&prevented&&!page.dirty){
|
|
3307
|
+
if(page.needsPaint)scheduleFrame();
|
|
3308
|
+
return {x:viewport.x,y:viewport.y};
|
|
3309
|
+
}
|
|
3041
3310
|
if (refresh() || prevented || activated || inputHandled) return { x: viewport.x, y: viewport.y };
|
|
3042
3311
|
}
|
|
3312
|
+
// CSS Overflow §3.3: root overflow applies to the viewport, with the
|
|
3313
|
+
// HTML body's overflow propagated when the root has visible overflow.
|
|
3314
|
+
// Hidden/clip must suppress keyboard scrolling, while scrollTo remains
|
|
3315
|
+
// available for hidden overflow. A game's Space key is not special.
|
|
3316
|
+
if (!viewportAllowsUserScroll("y")) return {x:viewport.x,y:viewport.y};
|
|
3043
3317
|
const pageStep = Math.max(31, viewport.height - 31);
|
|
3044
3318
|
const delta = {
|
|
3045
3319
|
ArrowUp: -31,
|
|
@@ -3113,6 +3387,11 @@ function buildPageRenderer({
|
|
|
3113
3387
|
const target = elementAtPoint(page.renderTree, page.displayList, Number(x), Number(y), viewport) ?? (pointerButtons ? page.document.documentElement : null);
|
|
3114
3388
|
const inside = target?.tagName === 'IFRAME' ? frameAt(target, x, y) : null;
|
|
3115
3389
|
if (inside) inside.frame.renderer.move(inside.x, inside.y, options);
|
|
3390
|
+
if (htmlDrag?.move(target,Number(x),Number(y),options)) {
|
|
3391
|
+
selectionDrag=null;
|
|
3392
|
+
if(page.dirty || page.needsPaint)scheduleFrame();
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3116
3395
|
dispatchPointerMove(page.window, page.realm, target, Number(x), Number(y), {...options, buttons: pointerButtons, suppressMouse:suppressPointerMouse});
|
|
3117
3396
|
if(selectionDrag && (pointerButtons&1)) extendDrag(x,y);
|
|
3118
3397
|
if (page.dirty || page.needsPaint) scheduleFrame();
|
|
@@ -3127,6 +3406,7 @@ function buildPageRenderer({
|
|
|
3127
3406
|
return this.click(x, y, {...options, button: button === 273 ? 'right' : button === 274 ? 'middle' : 'left', phase: state ? 'down' : 'up'});
|
|
3128
3407
|
},
|
|
3129
3408
|
click(x, y, options = {}) {
|
|
3409
|
+
clipboardActivationUntil = Date.now() + 5000;
|
|
3130
3410
|
const detail = Number.isInteger(options.clickCount) && options.clickCount > 0 ? options.clickCount : 1;
|
|
3131
3411
|
refresh();
|
|
3132
3412
|
let region = hitTest(page.renderTree, page.layout, x, y, viewport, viewport);
|
|
@@ -3211,13 +3491,24 @@ function buildPageRenderer({
|
|
|
3211
3491
|
if (before !== 0) page.realm.dispatch(target,new Pointer('pointermove',pointer));
|
|
3212
3492
|
if (!record.suppressMouse) {
|
|
3213
3493
|
const prevented = page.realm.dispatch(target,new page.window.MouseEvent('mousedown',{...mouseInit,buttons:pointerButtons}));
|
|
3214
|
-
if (mouseButton === 0 && !prevented)
|
|
3494
|
+
if (mouseButton === 0 && !prevented) {
|
|
3495
|
+
focus();
|
|
3496
|
+
let source=target;
|
|
3497
|
+
while(source && !isDraggable(source))source=source.parentElement;
|
|
3498
|
+
if(source)htmlDrag=new HTMLDrag(page.window,page.realm,source,Number(x),Number(y));
|
|
3499
|
+
}
|
|
3215
3500
|
}
|
|
3216
3501
|
if (mouseButton === 2) page.realm.dispatch(target,new Pointer('contextmenu',pointer));
|
|
3217
3502
|
} else if (mouseButton === 0) focus();
|
|
3218
3503
|
refresh();
|
|
3219
3504
|
return null;
|
|
3220
3505
|
}
|
|
3506
|
+
if(mouseButton===0 && htmlDrag){
|
|
3507
|
+
const drag=htmlDrag;htmlDrag=null;
|
|
3508
|
+
if(drag.end(Number(x),Number(y),options)){
|
|
3509
|
+
pointerPresses.delete(mouseButton);selectionDrag=null;suppressPointerMouse=false;refresh();return null;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3221
3512
|
let selectedByDrag=false;
|
|
3222
3513
|
if(mouseButton===0){
|
|
3223
3514
|
if(selectionDrag){extendDrag(x,y);selectedByDrag=!selectionDrag?.element&&!page.window.getSelection().isCollapsed;}
|
|
@@ -3287,6 +3578,7 @@ function buildPageRenderer({
|
|
|
3287
3578
|
if (prevented) return null;
|
|
3288
3579
|
}
|
|
3289
3580
|
if (options.selectionDragged) return null;
|
|
3581
|
+
if(target.tagName==='INPUT'&&target.type==='file'){void chooseInputFiles(target,dialog?.pickFiles,page.window).catch(error=>console.error(error));return null;}
|
|
3290
3582
|
if (generatedDisclosure) {
|
|
3291
3583
|
target.open = !target.open;
|
|
3292
3584
|
refresh();
|
|
@@ -3386,6 +3678,7 @@ function buildPageRenderer({
|
|
|
3386
3678
|
if (disposedRenderer) return;
|
|
3387
3679
|
resetInputCaret();
|
|
3388
3680
|
disposedRenderer = true;
|
|
3681
|
+
cssAnimations.dispose();
|
|
3389
3682
|
resourceElements?.dispose();
|
|
3390
3683
|
resourceElements = null;
|
|
3391
3684
|
loadGeneration++;
|
|
@@ -3402,6 +3695,7 @@ function buildPageRenderer({
|
|
|
3402
3695
|
rasterSurface?.dispose();
|
|
3403
3696
|
rasterSurface = null;
|
|
3404
3697
|
rasterReadback.dispose();
|
|
3698
|
+
canvasComposer.close();canvasLayers=null;canvasPixels=new WeakMap();clippedCanvasPixels=new WeakMap();
|
|
3405
3699
|
clearPaintParagraphs();
|
|
3406
3700
|
documentFonts.dispose();
|
|
3407
3701
|
page.window?.happyDOM?.close?.();
|
|
@@ -3490,7 +3784,8 @@ function buildPageRenderer({
|
|
|
3490
3784
|
beginImageLoad(element, src);
|
|
3491
3785
|
try {
|
|
3492
3786
|
const resource = await loadImageResource(document, new URL(src, element.baseURI || documentUrl).href, imageCrossOrigin(element));
|
|
3493
|
-
|
|
3787
|
+
// decode() resolves only once the renderer has installed these pixels
|
|
3788
|
+
// (replaceParsedDocument/awaitImages), so drawImage can use them then.
|
|
3494
3789
|
map.set(element, resource);
|
|
3495
3790
|
decoded.push(resource.image);
|
|
3496
3791
|
entries.push({ element, src, resource });
|
|
@@ -3562,6 +3857,7 @@ function buildPageRenderer({
|
|
|
3562
3857
|
requestDownload,
|
|
3563
3858
|
reportError,
|
|
3564
3859
|
pageFetch,
|
|
3860
|
+
dialog,
|
|
3565
3861
|
clipboardRead,
|
|
3566
3862
|
clipboardWrite,
|
|
3567
3863
|
clipboardReadInput,
|
|
@@ -3738,6 +4034,15 @@ function buildPageRenderer({
|
|
|
3738
4034
|
return buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf);
|
|
3739
4035
|
}
|
|
3740
4036
|
|
|
4037
|
+
function viewportAllowsUserScroll(axis) {
|
|
4038
|
+
const root=styleEngine.get(page.document?.documentElement);
|
|
4039
|
+
const body=styleEngine.get(page.document?.body);
|
|
4040
|
+
const style=root?.overflowX==="visible"&&root?.overflowY==="visible"&&body?.display!=="none" ? body??root : root;
|
|
4041
|
+
const overflow=axis==="x"?style?.overflowX:style?.overflowY;
|
|
4042
|
+
const scrolling=axis==="x"?style?.overflowScrollX:style?.overflowScrollY;
|
|
4043
|
+
return overflow!=="clip"||scrolling==="auto";
|
|
4044
|
+
}
|
|
4045
|
+
|
|
3741
4046
|
function scrollOffsetOf(element) {
|
|
3742
4047
|
return element ? page.elementScroll.get(element) ?? null : null;
|
|
3743
4048
|
}
|
|
@@ -3954,7 +4259,7 @@ function buildPageRenderer({
|
|
|
3954
4259
|
function mediaState(element) {
|
|
3955
4260
|
const state = page.media.get(element);
|
|
3956
4261
|
const decoder = state?.video?.decoder;
|
|
3957
|
-
return { paused: !state?.playing, ended: Boolean(state?.ended), currentTime: state ?
|
|
4262
|
+
return { paused: !state?.playing, ended: Boolean(state?.ended), currentTime: state ? presentedMediaTime(state) : 0,
|
|
3958
4263
|
duration: state?.duration ?? NaN, videoWidth: decoder?.width || 0, videoHeight: decoder?.height || 0 };
|
|
3959
4264
|
}
|
|
3960
4265
|
|
|
@@ -4162,6 +4467,15 @@ function buildPageRenderer({
|
|
|
4162
4467
|
return Number.isFinite(state.duration) ? Math.min(state.duration, time) : time;
|
|
4163
4468
|
}
|
|
4164
4469
|
|
|
4470
|
+
// Keep the decode target clock separate from the observable playback
|
|
4471
|
+
// position. A silent software decoder may lag behind real time; controls
|
|
4472
|
+
// must describe the picture actually presented, not its catch-up target.
|
|
4473
|
+
function presentedMediaTime(state) {
|
|
4474
|
+
const target = mediaTime(state);
|
|
4475
|
+
if (!state.playing || state.hasAudio || !state.video) return target;
|
|
4476
|
+
return Math.min(target, Math.max(state.base, state.video.shownTime ?? state.base));
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4165
4479
|
/** Fires a media event (play, pause, ended, seeked, timeupdate...) at the element. */
|
|
4166
4480
|
function mediaEvent(state, type) {
|
|
4167
4481
|
if (state.document !== page.document || !page.realm) return;
|
|
@@ -4179,8 +4493,11 @@ function buildPageRenderer({
|
|
|
4179
4493
|
&& [...page.media.values()].some(state => state.video?.presentation && state.playing)) {
|
|
4180
4494
|
// Another media element can still own a native progress/control bar.
|
|
4181
4495
|
// Its mutable resource time is not a DOM mutation, so invalidate paint.
|
|
4182
|
-
|
|
4183
|
-
|
|
4496
|
+
const controlsTime = hostPerformance.now();
|
|
4497
|
+
if (controlsTime - (page.lastMediaControlsPaint ?? -Infinity) >= 250
|
|
4498
|
+
&& page.displayList?.items.some(item => item.op === "drawAudio"
|
|
4499
|
+
|| (item.op === "drawVideo" && item.controls))) {
|
|
4500
|
+
page.lastMediaControlsPaint = controlsTime;
|
|
4184
4501
|
rasterized.list = null;
|
|
4185
4502
|
page.needsPaint = true;
|
|
4186
4503
|
}
|
|
@@ -4254,7 +4571,7 @@ function buildPageRenderer({
|
|
|
4254
4571
|
void (async () => {
|
|
4255
4572
|
let decoder;
|
|
4256
4573
|
try {
|
|
4257
|
-
decoder = await videoDecoder.decode(bytes, decodeOptions(state.videoMime || state.contentType, state.url));
|
|
4574
|
+
decoder = timeVideoDecoder(await videoDecoder.decode(bytes, decodeOptions(state.videoMime || state.contentType, state.url)), bytes);
|
|
4258
4575
|
if (!current()) { decoder.close(); decoder = null; failVideoContinuation(state, p); return; }
|
|
4259
4576
|
if (decoder.width !== video.decoder.width || decoder.height !== video.decoder.height || decoder.fps !== video.decoder.fps)
|
|
4260
4577
|
throw new Error("video snapshot configuration changed");
|
|
@@ -4301,7 +4618,7 @@ function buildPageRenderer({
|
|
|
4301
4618
|
const state = [...page.media.values()].find(state => state.resource === resource);
|
|
4302
4619
|
const frame = state?.video?.presentation?.frame;
|
|
4303
4620
|
if (!frame?.opaque || !state.playing || state.video.done || state.buffering || state.presentationFailed
|
|
4304
|
-
|| state.document !== page.document || !state.element.isConnected || item.
|
|
4621
|
+
|| state.document !== page.document || !state.element.isConnected || item.fixed) return null;
|
|
4305
4622
|
if (!videoLayerGeometry(item,region,page.deviceScaleFactor).replayable) return null;
|
|
4306
4623
|
return {resource,frame};
|
|
4307
4624
|
}
|
|
@@ -4312,9 +4629,9 @@ function buildPageRenderer({
|
|
|
4312
4629
|
const item = items.length === 1 ? items[0] : null;
|
|
4313
4630
|
const resource = item && page.displayList.images[item.image];
|
|
4314
4631
|
const state = [...page.media.values()].find(state => state.resource === resource);
|
|
4315
|
-
const eligible = state?.playing && state.video && !state.video.done && !state.buffering
|
|
4316
|
-
&& !state.presentationFailed && !item.
|
|
4317
|
-
&& (!item.transform || (item.transform.a > 0 && item.transform.d > 0));
|
|
4632
|
+
const eligible = state?.playing && state.video && Number.isFinite(state.video.shownTime) && !state.video.done && !state.buffering
|
|
4633
|
+
&& !state.presentationFailed && !item.fixed
|
|
4634
|
+
&& (!item.transform || (item.transform.a > 0 && item.transform.d > 0 && !item.transform.b && !item.transform.c));
|
|
4318
4635
|
for (const other of page.media.values()) if (!eligible || other !== state) stopVideoPresentation(other);
|
|
4319
4636
|
if (!eligible) return null;
|
|
4320
4637
|
const video = state.video;
|
|
@@ -4615,13 +4932,17 @@ function buildPageRenderer({
|
|
|
4615
4932
|
const bytes = selected?.bytes ?? snapshot(pipe, state.bytes);
|
|
4616
4933
|
if (!bytes?.byteLength) throw new Error(`media ${state.url}: no video bytes`);
|
|
4617
4934
|
state.video?.decoder.close();
|
|
4618
|
-
const decoder = await videoDecoder.decode(bytes, decodeOptions(state.videoMime || state.contentType, state.url));
|
|
4935
|
+
const decoder = timeVideoDecoder(await videoDecoder.decode(bytes, decodeOptions(state.videoMime || state.contentType, state.url)), bytes);
|
|
4619
4936
|
state.video = { decoder, bytes, frameTime: selected?.startTime ?? 0, done: false, pipe, openedGeneration: generation,
|
|
4620
4937
|
snapshotId: selected?.id, startTime: selected?.startTime ?? 0, endTime: selected?.endTime ?? Infinity };
|
|
4621
4938
|
trace?.(`video decoded ${state.url}: ${decoder.width}x${decoder.height}`);
|
|
4622
4939
|
if (state.resource.width !== decoder.width || state.resource.height !== decoder.height) {
|
|
4623
|
-
|
|
4624
|
-
|
|
4940
|
+
// Intrinsic layout caches key resources by identity. Publish a new
|
|
4941
|
+
// record on a size change; ordinary video-frame updates stay mutable.
|
|
4942
|
+
state.resource = {...state.resource, width:decoder.width, height:decoder.height};
|
|
4943
|
+
page.resources.set(state.element,state.resource);
|
|
4944
|
+
// Metadata changes intrinsic sizing even when no DOM/CSS changed.
|
|
4945
|
+
page.resourceLayoutDirty = true;
|
|
4625
4946
|
page.dirty = true;
|
|
4626
4947
|
refresh();
|
|
4627
4948
|
}
|
|
@@ -4790,7 +5111,7 @@ function buildPageRenderer({
|
|
|
4790
5111
|
function pauseMedia(element) {
|
|
4791
5112
|
const state = page.media.get(element);
|
|
4792
5113
|
if (!state?.playing) return;
|
|
4793
|
-
state.base =
|
|
5114
|
+
state.base = presentedMediaTime(state);
|
|
4794
5115
|
trace?.(`media pause at ${state.base.toFixed(3)}s`);
|
|
4795
5116
|
stopClock(state);
|
|
4796
5117
|
state.needsPosition = true;
|
|
@@ -4823,7 +5144,7 @@ function buildPageRenderer({
|
|
|
4823
5144
|
return;
|
|
4824
5145
|
}
|
|
4825
5146
|
if (wasPlaying && state.playing && state.document === page.document) startMedia(state);
|
|
4826
|
-
state.resource.time =
|
|
5147
|
+
state.resource.time = presentedMediaTime(state);
|
|
4827
5148
|
repaintMedia();
|
|
4828
5149
|
mediaEvent(state, "seeked");
|
|
4829
5150
|
mediaEvent(state, "timeupdate");
|
|
@@ -5003,7 +5324,11 @@ function buildPageRenderer({
|
|
|
5003
5324
|
} else if (video && !video.done && video.frameTime <= now) {
|
|
5004
5325
|
// Late frames are decoded but only the newest is shown.
|
|
5005
5326
|
const decodeDeadline = hostPerformance.now() + 100;
|
|
5006
|
-
|
|
5327
|
+
// A silent video uses a duration-clamped wall clock. Once it reaches
|
|
5328
|
+
// the advertised duration, finish decoding rather than abandoning the
|
|
5329
|
+
// tail (or waiting forever on rounding beyond that clock).
|
|
5330
|
+
const drainSilentTail=!state.hasAudio&&Number.isFinite(state.duration)&&now>=state.duration;
|
|
5331
|
+
while ((video.frameTime <= mediaTime(state)||drainSilentTail) && !video.done) {
|
|
5007
5332
|
const current = mediaTime(state);
|
|
5008
5333
|
const nextIsLate = video.frameTime + 1 / (video.decoder.fps || 30) <= current;
|
|
5009
5334
|
// If this tick has spent its decode budget, decode one final frame
|
|
@@ -5016,7 +5341,7 @@ function buildPageRenderer({
|
|
|
5016
5341
|
const reached = video.frameTime + 1e-6 >= (video.hideUntil ?? 0);
|
|
5017
5342
|
let step;
|
|
5018
5343
|
try {
|
|
5019
|
-
step = drawVideoFrame(state, reached && (!nextIsLate || yielding));
|
|
5344
|
+
step = drawVideoFrame(state, reached && (!Number.isFinite(video.shownTime) || !nextIsLate || yielding));
|
|
5020
5345
|
} catch (error) {
|
|
5021
5346
|
failMedia(state, error, 3);
|
|
5022
5347
|
return;
|
|
@@ -5029,17 +5354,18 @@ function buildPageRenderer({
|
|
|
5029
5354
|
// Keep tasks responsive even when software decoding is slower than
|
|
5030
5355
|
// real time. The zero-delay continuation resumes catching up against
|
|
5031
5356
|
// a freshly read clock instead of accumulating this task's duration.
|
|
5032
|
-
if (yielding && video.frameTime <= mediaTime(state)) break;
|
|
5357
|
+
if (yielding && (video.frameTime <= mediaTime(state)||drainSilentTail)) break;
|
|
5033
5358
|
}
|
|
5034
5359
|
}
|
|
5035
|
-
|
|
5360
|
+
const presented = presentedMediaTime(state);
|
|
5361
|
+
state.resource.time = presented;
|
|
5036
5362
|
if (!state.buffering && !state.awaitingAudio) {
|
|
5037
5363
|
playbackEvent(state, "playing", mediaEvent);
|
|
5038
5364
|
if (!state.playing) return;
|
|
5039
5365
|
}
|
|
5040
5366
|
repaintMedia();
|
|
5041
|
-
if (Math.floor(
|
|
5042
|
-
state.lastTimeUpdate = Math.floor(
|
|
5367
|
+
if (Math.floor(presented * 4) !== state.lastTimeUpdate) {
|
|
5368
|
+
state.lastTimeUpdate = Math.floor(presented * 4);
|
|
5043
5369
|
mediaEvent(state, "timeupdate");
|
|
5044
5370
|
}
|
|
5045
5371
|
const videoFinished = !video || video.done;
|
|
@@ -5062,7 +5388,9 @@ function buildPageRenderer({
|
|
|
5062
5388
|
void refillMedia(state, stalled);
|
|
5063
5389
|
return;
|
|
5064
5390
|
}
|
|
5065
|
-
|
|
5391
|
+
// Silent software decoding can lag behind wall time. Do not replace an
|
|
5392
|
+
// unfinished picture with ended solely because its duration elapsed.
|
|
5393
|
+
if ((videoFinished && soundFinished) || (pastEnd && soundFinished && state.hasAudio)) return endMedia(state);
|
|
5066
5394
|
const delay = video && !video.done ? Math.max(0, (video.frameTime - mediaTime(state)) * 1000) : 250;
|
|
5067
5395
|
state.timer = hostSetTimeout(() => mediaTick(state), delay);
|
|
5068
5396
|
}
|
|
@@ -5085,11 +5413,22 @@ function buildPageRenderer({
|
|
|
5085
5413
|
if (needsMore(video)) return "stall";
|
|
5086
5414
|
throw error;
|
|
5087
5415
|
}
|
|
5088
|
-
if (!pixels)
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5416
|
+
if (!pixels) {
|
|
5417
|
+
if(needsMore(video))return "stall";
|
|
5418
|
+
// Late intermediate frames may be skipped, but EOF must retain the
|
|
5419
|
+
// last decoded picture rather than whichever older frame was painted.
|
|
5420
|
+
if(video.pendingFrame){paintDecodedVideoFrame(state,video.pendingFrame);video.pendingFrame=null;}
|
|
5421
|
+
return false;
|
|
5422
|
+
}
|
|
5423
|
+
const shownTime = decoder.frameTime ?? state.video.frameTime;
|
|
5424
|
+
state.video.frameTime = decoder.nextFrameTime ?? (state.video.frameTime + 1 / (decoder.fps || 30));
|
|
5425
|
+
if (!show) {video.pendingFrame={pixels,width,height,time:shownTime};return true;}
|
|
5426
|
+
video.pendingFrame=null;
|
|
5427
|
+
return paintDecodedVideoFrame(state,{pixels,width,height,time:shownTime});
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
function paintDecodedVideoFrame(state,{pixels,width,height,time}) {
|
|
5431
|
+
state.video.shownTime = time;
|
|
5093
5432
|
const image = CanvasKit.MakeImage({ width, height, alphaType: CanvasKit.AlphaType.Unpremul,
|
|
5094
5433
|
colorType: CanvasKit.ColorType.RGBA_8888, colorSpace: CanvasKit.ColorSpace.SRGB }, pixels, width * 4);
|
|
5095
5434
|
if (!image) return true;
|
|
@@ -5438,6 +5777,11 @@ function buildPageRenderer({
|
|
|
5438
5777
|
|
|
5439
5778
|
function endMedia(state) {
|
|
5440
5779
|
stopClock(state);
|
|
5780
|
+
// Ended elements can remain reachable indefinitely (e.g. game effects).
|
|
5781
|
+
// Keep metadata and the final video image, but release the bounded WASM
|
|
5782
|
+
// audio session; loadMedia recreates it for a later play or seek.
|
|
5783
|
+
state.sound?.decoder.close();
|
|
5784
|
+
state.sound = null;
|
|
5441
5785
|
state.ended = true;
|
|
5442
5786
|
state.base = Number.isFinite(state.duration) ? state.duration : state.base;
|
|
5443
5787
|
setMediaPlaying(state, false);
|
|
@@ -5448,7 +5792,7 @@ function buildPageRenderer({
|
|
|
5448
5792
|
function setMediaPlaying(state, playing) {
|
|
5449
5793
|
state.playing = playing;
|
|
5450
5794
|
state.resource.playing = playing;
|
|
5451
|
-
state.resource.time =
|
|
5795
|
+
state.resource.time = presentedMediaTime(state);
|
|
5452
5796
|
if (state.document === page.document) repaintMedia();
|
|
5453
5797
|
}
|
|
5454
5798
|
|
|
@@ -5548,7 +5892,7 @@ function buildPageRenderer({
|
|
|
5548
5892
|
// Infinite feeds remove old DOM images without navigating. The resource
|
|
5549
5893
|
// WeakMap alone cannot release their explicitly-owned Skia handles.
|
|
5550
5894
|
for (const element of page.imageElements) {
|
|
5551
|
-
if (element.isConnected && element.ownerDocument === document && element.getAttribute("src")) continue;
|
|
5895
|
+
if ((element.isConnected || page.scriptImages.has(element)) && element.ownerDocument === document && element.getAttribute("src")) continue;
|
|
5552
5896
|
const image = page.resources.get(element)?.image;
|
|
5553
5897
|
if (image) {
|
|
5554
5898
|
staleImages.push(image);
|
|
@@ -5558,13 +5902,17 @@ function buildPageRenderer({
|
|
|
5558
5902
|
page.imageElements.delete(element);
|
|
5559
5903
|
imageRequests.delete(element);
|
|
5560
5904
|
}
|
|
5561
|
-
for (const element of resourceElements.get("images")) {
|
|
5905
|
+
for (const element of new Set([...resourceElements.get("images"),...page.scriptImages])) {
|
|
5562
5906
|
const src = element.getAttribute("src");
|
|
5563
5907
|
const credentials = imageCrossOrigin(element);
|
|
5564
5908
|
const key = imageRequestKey(src, credentials);
|
|
5565
5909
|
if (!src || imageRequests.get(element) === key) continue;
|
|
5566
5910
|
let url;
|
|
5567
|
-
try { url = new URL(src, element.baseURI || page.url).href; } catch {
|
|
5911
|
+
try { url = new URL(src, element.baseURI || page.url).href; } catch {
|
|
5912
|
+
imageRequests.set(element,key);
|
|
5913
|
+
finishImageLoad(element,src,null,type=>element.dispatchEvent(new page.window.Event(type)));
|
|
5914
|
+
continue;
|
|
5915
|
+
}
|
|
5568
5916
|
const existing = page.resources.get(element);
|
|
5569
5917
|
if (existing?.requestedURL === url && existing.crossOrigin === credentials) {
|
|
5570
5918
|
imageRequests.set(element, key);
|