buninu-browser 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/jsmdcui.txt +22 -0
- package/README.md +40 -4
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +152 -0
- package/frontends/wayland/casty/LICENSE +21 -0
- package/frontends/wayland/casty/bookmarks.js +32 -0
- package/frontends/wayland/casty/click.js +96 -0
- package/frontends/wayland/casty/commands.js +61 -0
- package/frontends/wayland/casty/config.js +45 -0
- package/frontends/wayland/casty/download-directory.js +28 -0
- package/frontends/wayland/casty/download.js +49 -0
- package/frontends/wayland/casty/hints.js +334 -0
- package/frontends/wayland/casty/keys.js +56 -0
- package/frontends/wayland/casty/page-commands.js +80 -0
- package/frontends/wayland/casty/prompt-width.js +148 -0
- package/frontends/wayland/casty/urlbar.js +606 -0
- package/frontends/wayland/client.js +88 -0
- package/frontends/wayland/clipboard.js +80 -0
- package/frontends/wayland/index.js +130 -0
- package/frontends/wayland/input.js +75 -0
- package/frontends/wayland/protocol.js +55 -0
- package/frontends/wayland/protocols/wayland.xml +3248 -0
- package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
- package/frontends/wayland/transport.js +120 -0
- package/frontends/wayland/urlbar.js +162 -0
- package/index.js +28 -1
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +37 -2
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +54 -12
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +6 -1
- package/lib/qml/README.md +1427 -0
- package/lib/qml/builtins.js +771 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +1199 -0
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +352 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +521 -0
- package/lib/qml/media-renderer.js +49 -0
- package/lib/qml/media.js +86 -0
- package/lib/qml/models.js +622 -0
- package/lib/qml/parser.js +381 -0
- package/lib/qml/qs +5 -0
- package/lib/qml/qscasty +3 -0
- package/lib/qml/qsheadless +3 -0
- package/lib/qml/qswayland +3 -0
- package/lib/qml/reactive.js +182 -0
- package/lib/qml/runtime.js +3603 -0
- package/lib/qml/shader/fragment.js +83 -0
- package/lib/qml/shader/qsb.js +27 -0
- package/lib/qml/shader/renderer.js +39 -0
- package/lib/qml/shader/runtime.js +61 -0
- package/lib/qml/shapes.js +251 -0
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +126 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/window-constraints.js +16 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +16 -1
- package/lib/renderer/canvas-2d.js +29 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +45 -17
- package/lib/renderer/main.js +21 -6
- package/lib/renderer/page-realm.js +20 -10
- package/lib/renderer/page-renderer.js +497 -42
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +10 -4
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/clipboard.test.js +19 -0
- package/test/computed-style.test.js +12 -0
- package/test/document-selection.test.js +27 -0
- package/test/double-click.test.js +85 -0
- package/test/fixtures/canvas-damage.js +40 -0
- package/test/fixtures/fake-renderer.js +1 -0
- package/test/fixtures/flea-helpers/Dockerfile +36 -0
- package/test/fixtures/flea-helpers/bat.mjs +43 -0
- package/test/fixtures/flea-helpers/bin/bat +3 -0
- package/test/fixtures/flea-helpers/bin/bwrap +10 -0
- package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
- package/test/fixtures/flea-helpers/bin/glow +3 -0
- package/test/fixtures/flea-helpers/bin/qs +3 -0
- package/test/fixtures/flea-helpers/bin/unsupported +3 -0
- package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
- package/test/fixtures/flea-helpers/check-media.mjs +29 -0
- package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
- package/test/fixtures/flea-helpers/check.mjs +36 -0
- package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
- package/test/fixtures/flea-helpers/source-mime.xml +28 -0
- package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
- package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
- package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
- package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
- package/test/fixtures/omastorm-helpers/check.mjs +56 -0
- package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
- package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
- package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
- package/test/fixtures/pointer-paint-batching.js +23 -0
- package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea/LICENSE +21 -0
- package/test/fixtures/qml/flea/README.md +25 -0
- package/test/fixtures/qml/flea/Theme.qml +8 -0
- package/test/fixtures/qml/flea/demo.qml +24 -0
- package/test/fixtures/qml/flea/qmldir +3 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
- package/test/fixtures/qml/flea-theme/README.md +70 -0
- package/test/fixtures/qml/flea-theme/button.qml +17 -0
- package/test/fixtures/qml/flea-theme/demo.qml +27 -0
- package/test/fixtures/qml/flea-theme/field.qml +21 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
- package/test/fixtures/qml/flea-theme/segment.qml +17 -0
- package/test/fixtures/qml/flea-theme/settings.qml +58 -0
- package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
- package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
- package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
- package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
- package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
- package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
- package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
- package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
- package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
- package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
- package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
- package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
- package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
- package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
- package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
- package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
- package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
- package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
- package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
- package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
- package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
- package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
- package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
- package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
- package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
- package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
- package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
- package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
- package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
- package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
- package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
- package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
- package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
- package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
- package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
- package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
- package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
- package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
- package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
- package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
- package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
- package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
- package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
- package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
- package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
- package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
- package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
- package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
- package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
- package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
- package/test/fixtures/qml/hello.qml +26 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
- package/test/fixtures/qml/imports/components/Badge.qml +15 -0
- package/test/fixtures/qml/imports/logic/base.js +2 -0
- package/test/fixtures/qml/imports/logic/counter.js +4 -0
- package/test/fixtures/qml/imports/logic/math.js +3 -0
- package/test/fixtures/qml/imports/logic/shared.js +3 -0
- package/test/fixtures/qml/imports/main.qml +21 -0
- package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
- package/test/fixtures/qml/imports/theme/qmldir +2 -0
- package/test/fixtures/qml/multiply.qml +61 -0
- package/test/fixtures/qml-canvas-dom.js +90 -0
- package/test/fixtures/qml-fonts/main.qml +10 -0
- package/test/fixtures/qml-locale/check.mjs +6 -0
- package/test/fixtures/qml-locale/main.qml +8 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/qml-shader/grab.qml +14 -0
- package/test/fixtures/qml-shader/gradient.frag +5 -0
- package/test/fixtures/qml-shader/main.qml +14 -0
- package/test/fixtures/qml-shader/texture-grab.qml +25 -0
- package/test/fixtures/qml-shader/texture-size.frag +5 -0
- package/test/fixtures/qml-shader-check.mjs +24 -0
- package/test/fixtures/title-no-paint.js +20 -0
- package/test/helpers/flea-sandbox.js +48 -0
- package/test/helpers/quickshell-fixture.js +125 -0
- package/test/html-clipboard-wayland.test.js +47 -0
- package/test/index.test.js +13 -0
- package/test/manual-casty-without-qml.js +87 -0
- package/test/manual-double-click.html +21 -0
- package/test/manual-flea-application.js +49 -0
- package/test/manual-flea-binary.js +62 -0
- package/test/manual-flea-qml.js +9 -0
- package/test/manual-flea-settings.js +40 -0
- package/test/manual-flea-theme.js +40 -0
- package/test/manual-mouse-buttons.html +18 -0
- package/test/manual-omastorm-engine.mjs +19 -0
- package/test/manual-omastorm-shaders.mjs +28 -0
- package/test/manual-omastorm-software.mjs +25 -0
- package/test/manual-pointer-scroll.html +43 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-canvas.test.js +21 -0
- package/test/qml-clipboard-bridge.test.js +25 -0
- package/test/qml-clipboard-wayland.test.js +40 -0
- package/test/qml-connections-timing.test.js +95 -0
- package/test/qml-controls-window.test.js +182 -0
- package/test/qml-dynamic.test.js +401 -0
- package/test/qml-flea-app.test.js +146 -0
- package/test/qml-flea-backend.test.js +78 -0
- package/test/qml-flea-columns-backend.test.js +38 -0
- package/test/qml-flea-compile.test.js +78 -0
- package/test/qml-flea-launcher.test.js +88 -0
- package/test/qml-flea-markdown-backend.test.js +31 -0
- package/test/qml-flea-media.test.js +28 -0
- package/test/qml-flea-navigation-backend.test.js +35 -0
- package/test/qml-flea-navigation-timing.test.js +147 -0
- package/test/qml-flea-preview-backend.test.js +53 -0
- package/test/qml-flea-previews.test.js +163 -0
- package/test/qml-flea-rows.test.js +101 -0
- package/test/qml-flea-textures.test.js +59 -0
- package/test/qml-flea-view-switch.test.js +33 -0
- package/test/qml-flea-wayland.test.js +68 -0
- package/test/qml-flea.test.js +34 -0
- package/test/qml-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -0
- package/test/qml-host.test.js +238 -0
- package/test/qml-imports.test.js +102 -0
- package/test/qml-inherited-handlers.test.js +51 -0
- package/test/qml-input-selection.test.js +66 -0
- package/test/qml-interactions.test.js +309 -0
- package/test/qml-language.test.js +167 -0
- package/test/qml-launcher.test.js +54 -0
- package/test/qml-layout-text.test.js +205 -0
- package/test/qml-locale.test.js +45 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-nonvisual-visible.test.js +19 -0
- package/test/qml-persisted-delegates.test.js +45 -0
- package/test/qml-quickshell-controls.test.js +65 -0
- package/test/qml-quickshell-settings.test.js +62 -0
- package/test/qml-repeater.test.js +72 -0
- package/test/qml-shader-runtime.test.js +6 -0
- package/test/qml-shortcuts.test.js +39 -0
- package/test/qml-socket.test.js +60 -0
- package/test/qml-software-shader.test.js +55 -0
- package/test/qml-style-updates.test.js +124 -0
- package/test/qml-textures.test.js +66 -0
- package/test/qml-views.test.js +332 -0
- package/test/qml-window-update.test.js +36 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/render-interval.test.js +33 -0
- package/test/renderer-host.test.js +11 -0
- package/test/text-input-pointer.test.js +31 -0
- package/test/text-input-scroll.test.js +29 -0
- package/test/text-input.test.js +47 -0
- package/test/title-no-paint.test.js +6 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +61 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Serialized into the QML realm. Snapshot ownership stays in ordinary canvases.
|
|
2
|
+
export function createQmlTextures(api) {
|
|
3
|
+
const { elements, state, effect, untrack, signal, cleanup } = api;
|
|
4
|
+
const entries = new WeakMap(), refs = new WeakMap(), pending = new Set();
|
|
5
|
+
const reference = value => {
|
|
6
|
+
let ref = refs.get(value);
|
|
7
|
+
if (!ref) refs.set(value, ref = state({ count: 0, hidden: 0 }));
|
|
8
|
+
return ref;
|
|
9
|
+
};
|
|
10
|
+
function attach(value, delta, hide) {
|
|
11
|
+
if (!value) return;
|
|
12
|
+
const ref = reference(value);
|
|
13
|
+
ref.count += delta;
|
|
14
|
+
if (hide) ref.hidden += delta;
|
|
15
|
+
const element = elements.get(value);
|
|
16
|
+
if (element) {
|
|
17
|
+
if (ref.count) element.setAttribute('data-qml-texture-source', '');
|
|
18
|
+
else element.removeAttribute('data-qml-texture-source');
|
|
19
|
+
if (ref.hidden || (!value.visible && ref.count)) element.setAttribute('data-qml-source-hidden', '');
|
|
20
|
+
else element.removeAttribute('data-qml-source-hidden');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function make(v, type, element) {
|
|
24
|
+
if (type !== 'ShaderEffectSource') return;
|
|
25
|
+
if (!api.host) throw Error('ShaderEffectSource requires a local QML host renderer');
|
|
26
|
+
Object.assign(v, { sourceItem: null, sourceRect: { x: 0, y: 0, width: 0, height: 0 },
|
|
27
|
+
mipmap:false, smooth:true, textureSize: { width: 0, height: 0 }, hideSource: false, live: true,
|
|
28
|
+
scheduledUpdateCompleted: signal() });
|
|
29
|
+
const canvas = document.createElement('canvas');
|
|
30
|
+
canvas.width = canvas.height = 1;
|
|
31
|
+
canvas.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none';
|
|
32
|
+
canvas.setAttribute('data-qml-texture', '');
|
|
33
|
+
element.appendChild(canvas);
|
|
34
|
+
const entry = { canvas, revision: state({ value: 0 }), ready: false };
|
|
35
|
+
entries.set(v, entry);
|
|
36
|
+
let source = null, hiding = false, observer, frame, disposed = false, requested = true;
|
|
37
|
+
const queue = () => {
|
|
38
|
+
if (!disposed && frame === undefined) { pending.add(capture); frame = requestAnimationFrame(capture); }
|
|
39
|
+
};
|
|
40
|
+
const capture = () => {
|
|
41
|
+
if (frame !== undefined) cancelAnimationFrame(frame);
|
|
42
|
+
pending.delete(capture);
|
|
43
|
+
frame = undefined;
|
|
44
|
+
if (disposed || !source || !requested) return;
|
|
45
|
+
const input = elements.get(source);
|
|
46
|
+
if (!input?.isConnected || !(source.width > 0 && source.height > 0)) return;
|
|
47
|
+
const rect = v.sourceRect;
|
|
48
|
+
const sw = rect.width > 0 ? rect.width : source.width;
|
|
49
|
+
const sh = rect.height > 0 ? rect.height : source.height;
|
|
50
|
+
const w = Math.ceil(v.textureSize.width > 0 ? v.textureSize.width : sw);
|
|
51
|
+
const h = Math.ceil(v.textureSize.height > 0 ? v.textureSize.height : sh);
|
|
52
|
+
if (!(w > 0 && h > 0) || w > 8192 || h > 8192 || w * h > 16777216)
|
|
53
|
+
throw Error('QML texture exceeds snapshot size limit');
|
|
54
|
+
requested = false;
|
|
55
|
+
if (canvas.width !== w) canvas.width = w;
|
|
56
|
+
if (canvas.height !== h) canvas.height = h;
|
|
57
|
+
const ctx = canvas.getContext('2d');
|
|
58
|
+
ctx.imageSmoothingEnabled=v.smooth!==false;
|
|
59
|
+
ctx.clearRect(0, 0, w, h);
|
|
60
|
+
ctx.drawImage(input, rect.width > 0 ? rect.x : 0, rect.height > 0 ? rect.y : 0, sw, sh, 0, 0, w, h);
|
|
61
|
+
entry.ready = true;
|
|
62
|
+
entry.revision.value++;
|
|
63
|
+
// Only signal once pixels are actually in the destination canvas.
|
|
64
|
+
v.scheduledUpdateCompleted();
|
|
65
|
+
};
|
|
66
|
+
v.scheduleUpdate = () => { requested = true; queue(); };
|
|
67
|
+
effect(() => {
|
|
68
|
+
const next = v.sourceItem, hide = v.hideSource;
|
|
69
|
+
v.smooth;
|
|
70
|
+
if(v.mipmap)throw Error("ShaderEffectSource mipmaps are not implemented");
|
|
71
|
+
const live = v.live;
|
|
72
|
+
const size = v.textureSize, rect = v.sourceRect;
|
|
73
|
+
size.width; size.height; rect.x; rect.y; rect.width; rect.height;
|
|
74
|
+
if (next) { next.width; next.height; next.visible; }
|
|
75
|
+
untrack(() => {
|
|
76
|
+
if (next !== source || hide !== hiding) {
|
|
77
|
+
observer?.disconnect();
|
|
78
|
+
attach(source, -1, hiding);
|
|
79
|
+
source = next; hiding = hide;
|
|
80
|
+
attach(source, 1, hiding);
|
|
81
|
+
if (source) {
|
|
82
|
+
const input = elements.get(source);
|
|
83
|
+
if (!input) throw Error('ShaderEffectSource.sourceItem must be an Item');
|
|
84
|
+
observer = new MutationObserver(() => {
|
|
85
|
+
if (v.live) { requested = true; queue(); }
|
|
86
|
+
});
|
|
87
|
+
observer.observe(input, { subtree: true, attributes: true, childList: true, characterData: true });
|
|
88
|
+
} else {
|
|
89
|
+
entry.ready = false;
|
|
90
|
+
canvas.width = canvas.height = 1;
|
|
91
|
+
entry.revision.value++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (live || !entry.ready || requested) { requested = true; queue(); }
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
cleanup(() => {
|
|
98
|
+
disposed = true;
|
|
99
|
+
pending.delete(capture);
|
|
100
|
+
observer?.disconnect();
|
|
101
|
+
if (frame !== undefined) cancelAnimationFrame(frame);
|
|
102
|
+
untrack(() => attach(source, -1, hiding));
|
|
103
|
+
canvas.width = canvas.height = 1;
|
|
104
|
+
canvas.remove();
|
|
105
|
+
entries.delete(v);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function keepLayout(v) {
|
|
109
|
+
const ref = reference(v);
|
|
110
|
+
const keep = ref.count > 0;
|
|
111
|
+
const element = elements.get(v);
|
|
112
|
+
const hidden = ref.hidden > 0 || (keep && !v.visible);
|
|
113
|
+
if (element && element.hasAttribute('data-qml-source-hidden') !== hidden) {
|
|
114
|
+
if (hidden) element.setAttribute('data-qml-source-hidden', '');
|
|
115
|
+
else element.removeAttribute('data-qml-source-hidden');
|
|
116
|
+
}
|
|
117
|
+
return keep;
|
|
118
|
+
}
|
|
119
|
+
function image(v) {
|
|
120
|
+
const entry = entries.get(v);
|
|
121
|
+
if (!entry) throw Error('ShapePath.fillItem currently requires ShaderEffectSource');
|
|
122
|
+
entry.revision.value;
|
|
123
|
+
return entry.ready ? entry.canvas : null;
|
|
124
|
+
}
|
|
125
|
+
return { make, keepLayout, image, flush: () => { for (const capture of [...pending]) capture(); } };
|
|
126
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Optional local-QML bridge to the frontend's standard Wayland clipboard.
|
|
2
|
+
// Ordinary HTML never needs this HTTP endpoint. Create it lazily before the
|
|
3
|
+
// first local QML navigation, including navigation after an about:blank launch.
|
|
4
|
+
export async function attachQmlClipboard(client,browser,done) {
|
|
5
|
+
const clipboard=client.clipboard;
|
|
6
|
+
if(!clipboard)return;
|
|
7
|
+
const navigate=browser.context.navigate;
|
|
8
|
+
let server,setup,closed=false;
|
|
9
|
+
const prepare=()=>setup??=(async()=>{
|
|
10
|
+
const token=crypto.randomUUID();
|
|
11
|
+
server=Bun.serve({hostname:'127.0.0.1',port:0,maxRequestBodySize:1024*1024+1024,
|
|
12
|
+
async fetch(request) {
|
|
13
|
+
if(request.method!=='POST'||new URL(request.url).pathname!=='/'+token||request.headers.has('origin'))return new Response(null,{status:403});
|
|
14
|
+
try {
|
|
15
|
+
const {operation,text}=await request.json();
|
|
16
|
+
if(operation==='read')return Response.json({text:await clipboard.read()});
|
|
17
|
+
if(operation!=='write'||typeof text!=='string')return new Response(null,{status:400});
|
|
18
|
+
await clipboard.write(text);return Response.json({});
|
|
19
|
+
}catch{return new Response(null,{status:503});}
|
|
20
|
+
}});
|
|
21
|
+
await browser.context.addInitScript(`globalThis.__qmlHost?.setClipboardEndpoint(${JSON.stringify(`http://127.0.0.1:${server.port}/${token}`)});`);
|
|
22
|
+
})();
|
|
23
|
+
const wrapped=async function(url,...args) {
|
|
24
|
+
if(!closed&&/^file:.*\.qml(?:[?#]|$)/i.test(String(url)))await prepare();
|
|
25
|
+
return navigate.call(this,url,...args);
|
|
26
|
+
};
|
|
27
|
+
browser.context.navigate=wrapped;
|
|
28
|
+
done.finally(()=>{
|
|
29
|
+
closed=true;server?.stop(true);
|
|
30
|
+
if(browser.context.navigate===wrapped)browser.context.navigate=navigate;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Optional frontend adapter; xdg_toplevel size hints, not an application check.
|
|
2
|
+
export function qmlWindowConstraints(client,window,context) {
|
|
3
|
+
let previous='';
|
|
4
|
+
return async()=>{
|
|
5
|
+
if(!previous&&!/\.qml(?:[?#]|$)/i.test(context.url||''))return;
|
|
6
|
+
const hints=await context.evaluate(`(()=>{const elements=[...document.querySelectorAll('[data-qml-window-min]')];const e=elements.find(e=>e.style.display!=='none');return e?[e.getAttribute('data-qml-window-min'),e.getAttribute('data-qml-window-max')]:null;})()`);
|
|
7
|
+
const key=JSON.stringify(hints);if(previous===key)return;
|
|
8
|
+
const parse=value=>{try{return JSON.parse(value).map(n=>Number.isFinite(n)?Math.max(0,Math.min(8192,Math.ceil(n))):0);}catch{return [0,0];}};
|
|
9
|
+
const min=parse(hints?.[0]),max=parse(hints?.[1]);
|
|
10
|
+
for(let i=0;i<2;i++)if(max[i]&&max[i]<min[i])max[i]=min[i];
|
|
11
|
+
client.request(window.toplevel,'set_min_size',...min);
|
|
12
|
+
client.request(window.toplevel,'set_max_size',...max);
|
|
13
|
+
client.request(window.surface,'commit');
|
|
14
|
+
previous=key;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Local WorkerScript application code executes on its own Bun worker thread.
|
|
2
|
+
// Qt.include uses one script global scope; it is not the QML import resolver.
|
|
3
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { createContext, Script } from 'node:vm';
|
|
6
|
+
let current = workerData.url,
|
|
7
|
+
bytes = 0,
|
|
8
|
+
depth = 0;
|
|
9
|
+
const context = createContext({
|
|
10
|
+
console,
|
|
11
|
+
WorkerScript: {
|
|
12
|
+
onMessage: null,
|
|
13
|
+
sendMessage: (message) =>
|
|
14
|
+
parentPort.postMessage({ kind: 'message', message }),
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
const run = (url) => {
|
|
18
|
+
if (new URL(url).protocol !== 'file:')
|
|
19
|
+
throw Error('WorkerScript imports must be local files');
|
|
20
|
+
if (++depth > 48) throw Error('WorkerScript include depth exceeded');
|
|
21
|
+
const previous = current;
|
|
22
|
+
current = url;
|
|
23
|
+
try {
|
|
24
|
+
const source = readFileSync(new URL(url), 'utf8');
|
|
25
|
+
bytes += Buffer.byteLength(source);
|
|
26
|
+
if (bytes > 4 * 1024 * 1024)
|
|
27
|
+
throw Error('WorkerScript source limit exceeded');
|
|
28
|
+
// Like Qt.include in a WorkerScript, directives supply no import aliases.
|
|
29
|
+
const code = source.replace(
|
|
30
|
+
/^\s*\.(?:pragma\s+library|import\s+[^\n]+)\s*$/gm,
|
|
31
|
+
'',
|
|
32
|
+
);
|
|
33
|
+
new Script(code, { filename: url }).runInContext(context);
|
|
34
|
+
} finally {
|
|
35
|
+
current = previous;
|
|
36
|
+
depth--;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
context.Qt = {
|
|
40
|
+
include: (relative, callback) => {
|
|
41
|
+
let result;
|
|
42
|
+
try {
|
|
43
|
+
run(new URL(relative, current).href);
|
|
44
|
+
result = { status: 0 };
|
|
45
|
+
} catch (exception) {
|
|
46
|
+
result = { status: 3, exception };
|
|
47
|
+
}
|
|
48
|
+
callback?.(result);
|
|
49
|
+
return result;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
run(current);
|
|
54
|
+
parentPort.postMessage({ kind: 'ready' });
|
|
55
|
+
parentPort.on('message', (message) => {
|
|
56
|
+
try {
|
|
57
|
+
context.WorkerScript.onMessage?.(message);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
parentPort.postMessage({
|
|
60
|
+
kind: 'error',
|
|
61
|
+
error: String(error.stack || error),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
parentPort.postMessage({
|
|
67
|
+
kind: 'error',
|
|
68
|
+
error: String(error.stack || error),
|
|
69
|
+
});
|
|
70
|
+
}
|
package/lib/render-tree/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { extractSvg } from "./svg.js";
|
|
2
2
|
import { computeElementStyle, controlKind } from "../style/computed-style.js";
|
|
3
3
|
|
|
4
|
+
import {textInputSelection} from '../renderer/text-input-paint.js';
|
|
5
|
+
|
|
4
6
|
const PLACEHOLDER_COLOR = "rgba(117, 117, 117, 1)";
|
|
5
7
|
const CHECKED_COLOR = "rgba(0, 117, 255, 1)";
|
|
6
8
|
const DEFAULT_SUMMARY = Object.freeze({ tagName: "SUMMARY", getAttribute: () => null, hasAttribute: () => false });
|
|
@@ -99,6 +101,7 @@ export class RenderTreeBuilder {
|
|
|
99
101
|
let keys = this.#pseudoKeys.get(element);
|
|
100
102
|
if (!keys) this.#pseudoKeys.set(element, keys = { before: {}, after: {}, beforeText: {}, afterText: {} });
|
|
101
103
|
keys.value ??= {};
|
|
104
|
+
const controlType=boxType(style)==="inline"?"inline-block":boxType(style);
|
|
102
105
|
let boxStyle = style;
|
|
103
106
|
let text = "";
|
|
104
107
|
let textStyle = style;
|
|
@@ -134,10 +137,22 @@ export class RenderTreeBuilder {
|
|
|
134
137
|
textStyle = Object.freeze({ ...style, color: PLACEHOLDER_COLOR });
|
|
135
138
|
}
|
|
136
139
|
}
|
|
140
|
+
|
|
141
|
+
if (control === "text" || control === "textarea") {
|
|
142
|
+
const selected = textInputSelection(element, text, textStyle, (part, type, properties, domNode) => {
|
|
143
|
+
keys.inputSelection ??= {};
|
|
144
|
+
keys.inputSelection[part] ??= {};
|
|
145
|
+
return this.#node(domNode, type, nodesById, properties, keys.inputSelection[part]);
|
|
146
|
+
});
|
|
147
|
+
if (selected) return this.#node(element, controlType, nodesById, {
|
|
148
|
+
tagName: element.tagName, style: boxStyle, children: Object.freeze(selected),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
const children = text
|
|
138
153
|
? [this.#node(element, "text", nodesById, { text, style: textStyle, pseudo: "value", children: Object.freeze([]) }, keys.value)]
|
|
139
154
|
: [];
|
|
140
|
-
return this.#node(element,
|
|
155
|
+
return this.#node(element, controlType, nodesById, {
|
|
141
156
|
tagName: element.tagName,
|
|
142
157
|
style: boxStyle,
|
|
143
158
|
children: Object.freeze(children),
|
package/lib/renderer/bindings.js
CHANGED
|
@@ -21,6 +21,7 @@ const isCanvasSizeAttribute = (node, name, namespace = null) => !namespace
|
|
|
21
21
|
&& node.namespaceURI === "http://www.w3.org/1999/xhtml" && node.tagName === "CANVAS"
|
|
22
22
|
&& (name === "width" || name === "height");
|
|
23
23
|
const REFLECTED_PROPERTIES = [
|
|
24
|
+
"selectionStart", "selectionEnd", "selectionDirection", "maxLength",
|
|
24
25
|
"value", "checked", "disabled", "type", "name", "href", "src", "alt", "title",
|
|
25
26
|
"placeholder", "selected", "readOnly", "required", "min", "max", "step", "rel", "target",
|
|
26
27
|
"htmlFor", "lang", "dir", "tabIndex", "hidden", "defaultValue", "action", "method",
|
|
@@ -204,6 +205,8 @@ export class BindingRealm {
|
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
/** The page's Range for an engine Range (one wrapper each). */
|
|
208
|
+
unwrapRange(range) { return RangeBinding.rangeOf(range); }
|
|
209
|
+
|
|
207
210
|
wrapRange(range) {
|
|
208
211
|
if (!range) return null;
|
|
209
212
|
let wrapper = this.#ranges.get(range);
|
|
@@ -1097,6 +1100,16 @@ class ElementBinding extends ParentBinding {
|
|
|
1097
1100
|
|
|
1098
1101
|
get validity() { return this.#validity ??= realmOf(this).wrapValidity(nodeOf(this).validity); }
|
|
1099
1102
|
|
|
1103
|
+
setSelectionRange(start, end, direction = 'none') {
|
|
1104
|
+
if(arguments.length<2)throw new TypeError('setSelectionRange requires start and end');
|
|
1105
|
+
nodeOf(this).setSelectionRange(Number(start),Number(end),String(direction));touch(this);
|
|
1106
|
+
}
|
|
1107
|
+
setRangeText(replacement, ...args) {
|
|
1108
|
+
if(!arguments.length)throw new TypeError('setRangeText requires replacement');
|
|
1109
|
+
nodeOf(this).setRangeText(String(replacement),...args.map((v,i)=>i<2?Number(v):String(v)));touch(this);
|
|
1110
|
+
}
|
|
1111
|
+
select() {nodeOf(this).select();touch(this);}
|
|
1112
|
+
|
|
1100
1113
|
setCustomValidity(message) {
|
|
1101
1114
|
if (!arguments.length) throw new TypeError("setCustomValidity requires a message");
|
|
1102
1115
|
nodeOf(this).setCustomValidity(String(message));
|
|
@@ -1361,7 +1374,7 @@ function isMedia(wrapper) {
|
|
|
1361
1374
|
|
|
1362
1375
|
/** on* event handler properties (element.onclick = fn, script.onload = fn). */
|
|
1363
1376
|
export const EVENT_HANDLER_NAMES = Object.freeze([
|
|
1364
|
-
"click", "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave",
|
|
1377
|
+
"click", "auxclick", "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave",
|
|
1365
1378
|
"keydown", "keyup", "keypress", "beforeinput", "input", "change", "submit", "reset", "focus", "blur", "load", "error",
|
|
1366
1379
|
"abort", "scroll", "resize", "wheel", "contextmenu", "touchstart", "touchend", "touchmove",
|
|
1367
1380
|
"pointerdown", "pointerup", "pointermove", "animationend", "transitionend", "readystatechange", "slotchange", "toggle", "invalid",
|
|
@@ -1685,6 +1698,8 @@ class DocumentBinding extends ParentBinding {
|
|
|
1685
1698
|
return realmOf(this).wrap(nodeOf(this).createDocumentFragment());
|
|
1686
1699
|
}
|
|
1687
1700
|
|
|
1701
|
+
getSelection() { return realmOf(this).selection?.() ?? null; }
|
|
1702
|
+
|
|
1688
1703
|
createRange() {
|
|
1689
1704
|
return realmOf(this).wrapRange(nodeOf(this).createRange());
|
|
1690
1705
|
}
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* whole WASM heap. The copy handed back is only the requested rectangle.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
// qml handle begin: consume local-QML DOM snapshots without exposing native images
|
|
18
|
+
import { drawQmlDomImage } from "../qml/canvas-dom.js";
|
|
19
|
+
// qml handle end
|
|
20
|
+
|
|
17
21
|
let CanvasKit = null;
|
|
18
22
|
|
|
19
23
|
/** The CanvasKit `loadGraphics` instantiated. Page canvases share it. */
|
|
@@ -225,6 +229,15 @@ function open(node) {
|
|
|
225
229
|
if (property === "drawImage" && resolveImage) {
|
|
226
230
|
source = resolveImage(args[0]);
|
|
227
231
|
if (!source?.image) return;
|
|
232
|
+
|
|
233
|
+
// qml handle begin: temporary DOM images have synchronous ownership
|
|
234
|
+
if (drawQmlDomImage(source, state.node.deref(), context, args)) {
|
|
235
|
+
if (source.originClean === false) state.originClean = false;
|
|
236
|
+
changed(state);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// qml handle end
|
|
240
|
+
|
|
228
241
|
args[0] = source.image;
|
|
229
242
|
}
|
|
230
243
|
const result = context[property](...args);
|
|
@@ -333,3 +346,19 @@ function copyImageData(image) {
|
|
|
333
346
|
}
|
|
334
347
|
return { width: image.width, height: image.height, data, colorSpace: "srgb" };
|
|
335
348
|
}
|
|
349
|
+
|
|
350
|
+
// qml handle begin: software fragment effects draw into the existing canvas surface
|
|
351
|
+
export function paintQmlCanvas(node, draw) {
|
|
352
|
+
if (!canvasContext(node, "2d")) throw Error('Canvas renderer is unavailable');
|
|
353
|
+
const state = surfaces.get(node).state;
|
|
354
|
+
if (!state.raster) throw Error('Canvas surface is unavailable');
|
|
355
|
+
const canvas = state.raster.getCanvas();
|
|
356
|
+
canvas.save();
|
|
357
|
+
try {
|
|
358
|
+
const inverse = CanvasKit.Matrix.invert(canvas.getTotalMatrix());
|
|
359
|
+
if (!inverse) throw Error("Noninvertible QML canvas transform");
|
|
360
|
+
canvas.concat(inverse); draw(canvas);
|
|
361
|
+
}
|
|
362
|
+
finally { canvas.restore(); changed(state); }
|
|
363
|
+
}
|
|
364
|
+
// qml handle end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Keep engine nodes/ranges behind the same DOM wrappers as the rest of the realm.
|
|
2
|
+
export function selectionBinding(window, realm, nodeOf, changed) {
|
|
3
|
+
const native=()=>window.getSelection();
|
|
4
|
+
const api={};
|
|
5
|
+
for(const key of ['anchorOffset','focusOffset','isCollapsed','rangeCount','type'])Object.defineProperty(api,key,{get:()=>native()[key]});
|
|
6
|
+
for(const key of ['anchorNode','focusNode'])Object.defineProperty(api,key,{get:()=>realm.wrap(native()[key])});
|
|
7
|
+
api.toString=()=>native().toString();
|
|
8
|
+
api.getRangeAt=i=>realm.wrapRange(native().getRangeAt(i));
|
|
9
|
+
const mutate=(name,args)=>{const result=native()[name](...args);changed();return result;};
|
|
10
|
+
for(const name of ['removeAllRanges','empty','collapseToStart','collapseToEnd','deleteFromDocument'])api[name]=()=>mutate(name,[]);
|
|
11
|
+
api.collapse=api.setPosition=(node,offset=0)=>mutate('collapse',[nodeOf(node),offset]);
|
|
12
|
+
api.extend=(node,offset=0)=>mutate('extend',[nodeOf(node),offset]);
|
|
13
|
+
api.setBaseAndExtent=(a,ao,f,fo)=>mutate('setBaseAndExtent',[nodeOf(a),ao,nodeOf(f),fo]);
|
|
14
|
+
api.selectAllChildren=node=>mutate('selectAllChildren',[nodeOf(node)]);
|
|
15
|
+
api.containsNode=(node,partial=false)=>native().containsNode(nodeOf(node),partial);
|
|
16
|
+
api.addRange=range=>mutate('addRange',[realm.unwrapRange(range)]);
|
|
17
|
+
api.removeRange=range=>mutate('removeRange',[realm.unwrapRange(range)]);
|
|
18
|
+
return api;
|
|
19
|
+
}
|
package/lib/renderer/host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RENDER_INTERVAL_FLAG, renderMinInterval } from "./render-interval.js";
|
|
2
|
+
import {writeClipboard, readClipboard} from '../clipboard.js';
|
|
2
3
|
/**
|
|
3
4
|
* Controller-side handle for a renderer process.
|
|
4
5
|
*
|
|
@@ -16,6 +17,7 @@ import {writeClipboard} from '../clipboard.js';
|
|
|
16
17
|
import {listenCompositor} from "./compositor-channel.js";
|
|
17
18
|
import {RawFrames} from "../paint/raw-frames.js";
|
|
18
19
|
import { PulseClient } from "../audio/pulse-client.js";
|
|
20
|
+
import { SharedAudioConnection } from "../audio/shared-connection.js";
|
|
19
21
|
import { logEnabled, pipeToLog, RENDERER_LOG_FLAG, writeLog } from "../log.js";
|
|
20
22
|
import { NetworkService } from "../network/network-service.js";
|
|
21
23
|
import { COMPOSITOR_FLAG, FULL_STYLE_FLAG, VERIFY_STYLE_FLAG } from "./flags.js";
|
|
@@ -33,14 +35,9 @@ const MAX_RESOURCE_BYTES = 32 * 1024 * 1024;
|
|
|
33
35
|
// Audio output: one PulseAudio native-protocol connection (jspulse or a
|
|
34
36
|
// PulseAudio daemon on 127.0.0.1:4713) shared by every renderer, opened on
|
|
35
37
|
// first use. Renderers cannot open sockets, so their audio comes through here.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
pulse = null;
|
|
40
|
-
throw new Error(`no PulseAudio server at 127.0.0.1:4713 (start jspulse): ${error.message}`);
|
|
41
|
-
});
|
|
42
|
-
return pulse;
|
|
43
|
-
}
|
|
38
|
+
const pulse = new SharedAudioConnection(() => PulseClient.connect().catch((error) => {
|
|
39
|
+
throw new Error(`no PulseAudio server at 127.0.0.1:4713 (start jspulse): ${error.message}`);
|
|
40
|
+
}));
|
|
44
41
|
const MAX_AUDIO_STREAMS = 8;
|
|
45
42
|
const MAX_AUDIO_CHUNK = 4 * 1024 * 1024;
|
|
46
43
|
|
|
@@ -55,11 +52,13 @@ const LIFECYCLE_NAMES = new Set(["DOMContentLoaded", "load"]);
|
|
|
55
52
|
const DEFAULT_ENTRY = Bun.fileURLToPath(new URL("./main.js", import.meta.url));
|
|
56
53
|
|
|
57
54
|
export class RendererHost {
|
|
55
|
+
#clipboardRead = readClipboard;
|
|
58
56
|
#clipboardWrite = writeClipboard;
|
|
59
57
|
#clipboardUntil = 0;
|
|
60
58
|
#clipboardPending = false;
|
|
61
59
|
#rawFrames = null;
|
|
62
60
|
#startEpoch = 0;
|
|
61
|
+
#closed = false;
|
|
63
62
|
#rejectStartup = null;
|
|
64
63
|
#entry;
|
|
65
64
|
#args;
|
|
@@ -114,8 +113,10 @@ export class RendererHost {
|
|
|
114
113
|
network = new NetworkService({ fetch: fetchImpl }), onFrame = null, onLifecycle = null,
|
|
115
114
|
// A dedicated loopback socket works independently of the main thread on all platforms.
|
|
116
115
|
compositor = entry === DEFAULT_ENTRY,
|
|
116
|
+
clipboardRead = readClipboard,
|
|
117
117
|
clipboardWrite = writeClipboard,
|
|
118
118
|
} = {}) {
|
|
119
|
+
this.#clipboardRead = clipboardRead;
|
|
119
120
|
this.#clipboardWrite = clipboardWrite;
|
|
120
121
|
this.#onFrame = onFrame;
|
|
121
122
|
this.#onLifecycle = onLifecycle;
|
|
@@ -131,6 +132,7 @@ export class RendererHost {
|
|
|
131
132
|
|
|
132
133
|
/** Starts the renderer if it is not running and waits until it is ready. */
|
|
133
134
|
start() {
|
|
135
|
+
if (this.#closed) return Promise.reject(Error("Renderer is closed"));
|
|
134
136
|
if (this.#ready) return this.#ready;
|
|
135
137
|
let resolveReady;
|
|
136
138
|
let rejectReady;
|
|
@@ -149,13 +151,20 @@ export class RendererHost {
|
|
|
149
151
|
()=>this.#closeChannel(channel));
|
|
150
152
|
if (epoch !== this.#startEpoch) { channel?.transport.close(); throw Error('Renderer startup cancelled'); }
|
|
151
153
|
this.#channel = channel;
|
|
154
|
+
// No inherited environment: nothing from the controller leaks in.
|
|
155
|
+
// Windows programs need SystemRoot to load system libraries.
|
|
156
|
+
let env = process.platform === "win32" && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {};
|
|
157
|
+
|
|
158
|
+
// qml handle begin: applications need HOME/PATH/runtime paths, web pages do not
|
|
159
|
+
if (this.#args.includes("--buninu-qml-host")) env = { ...process.env };
|
|
160
|
+
// qml handle end
|
|
161
|
+
|
|
152
162
|
const child = Bun.spawn({
|
|
153
163
|
cmd: [process.execPath, this.#entry, ...this.#args,
|
|
164
|
+
RENDER_INTERVAL_FLAG + renderMinInterval(process.env.BUNINU_RENDER_MIN_INTERVAL),
|
|
154
165
|
...(logging ? [RENDERER_LOG_FLAG] : []), ...(compositor ? [COMPOSITOR_FLAG, `--compositor-endpoint=${JSON.stringify(channel.transport.endpoint)}`] : []),
|
|
155
166
|
...(process.env.BUNINU_VERIFY_STYLE ? [VERIFY_STYLE_FLAG] : []), ...(process.env.BUNINU_FULL_STYLE ? [FULL_STYLE_FLAG] : [])],
|
|
156
|
-
|
|
157
|
-
// Windows programs need SystemRoot to load system libraries.
|
|
158
|
-
env: process.platform === "win32" && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {},
|
|
167
|
+
env,
|
|
159
168
|
// With BUNINU_LOG, output goes through the controller into the log file.
|
|
160
169
|
stdio: ["ignore", logging ? "pipe" : "inherit", logging ? "pipe" : "inherit"],
|
|
161
170
|
serialization: "advanced",
|
|
@@ -297,6 +306,7 @@ export class RendererHost {
|
|
|
297
306
|
}
|
|
298
307
|
|
|
299
308
|
close() {
|
|
309
|
+
this.#closed = true;
|
|
300
310
|
this.#startEpoch++;
|
|
301
311
|
this.#rejectStartup?.(Error("Renderer closed during startup")); this.#rejectStartup=null;
|
|
302
312
|
this.#rawFrames?.close(); this.#rawFrames=null;
|
|
@@ -313,6 +323,7 @@ export class RendererHost {
|
|
|
313
323
|
}
|
|
314
324
|
|
|
315
325
|
async #call(method, params, timeout = this.#timeout) {
|
|
326
|
+
if (this.#closed) throw Error("Renderer is closed");
|
|
316
327
|
if (!this.#child) await this.#restart();
|
|
317
328
|
await this.start();
|
|
318
329
|
const child = this.#child;
|
|
@@ -365,11 +376,23 @@ export class RendererHost {
|
|
|
365
376
|
pending.reject(new Error(`renderer: ${String(message.error).split("\n")[0] || "unknown error"}`));
|
|
366
377
|
}
|
|
367
378
|
else pending.resolve(message.result);
|
|
368
|
-
} else if (
|
|
379
|
+
} else if (['clipboard-read','clipboard-read-input'].includes(message.type) && Number.isInteger(message.id)) {
|
|
380
|
+
const reply = (text, error) => {if(this.#child===child) child.send({type:'clipboard-result',id:message.id,text,error});};
|
|
381
|
+
let secure=false;
|
|
382
|
+
try {const url=new URL(this.#documentUrl); secure=url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname));} catch {}
|
|
383
|
+
if ((!secure && !message.type.endsWith('-input')) || Date.now()>this.#clipboardUntil || this.#clipboardPending) {
|
|
384
|
+
reply(undefined,'Clipboard read requires a recent user action in a secure document'); return;
|
|
385
|
+
}
|
|
386
|
+
this.#clipboardUntil=0; this.#clipboardPending=true;
|
|
387
|
+
Promise.resolve().then(()=>this.#clipboardRead()).then(text=>{
|
|
388
|
+
if(typeof text!=='string'||Buffer.byteLength(text)>1024*1024)throw Error('Clipboard text exceeds limit');
|
|
389
|
+
reply(text);
|
|
390
|
+
}).catch(()=>reply(undefined,'Unable to read system clipboard')).finally(()=>{this.#clipboardPending=false;});
|
|
391
|
+
} else if (['clipboard-write','clipboard-write-input'].includes(message.type) && Number.isInteger(message.id)) {
|
|
369
392
|
const reply = error => { if(this.#child===child)child.send({type:'clipboard-result',id:message.id,...(error?{error}: {})}); };
|
|
370
393
|
let secure=false;
|
|
371
394
|
try { const url=new URL(this.#documentUrl); secure=url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname)); } catch {}
|
|
372
|
-
if (!secure || Date.now()>this.#clipboardUntil || this.#clipboardPending
|
|
395
|
+
if ((!secure && !message.type.endsWith('-input')) || Date.now()>this.#clipboardUntil || this.#clipboardPending
|
|
373
396
|
|| typeof message.text!=='string' || Buffer.byteLength(message.text)>1024*1024) {
|
|
374
397
|
reply('Clipboard write requires a recent user action in a secure document'); return;
|
|
375
398
|
}
|
|
@@ -518,20 +541,24 @@ export class RendererHost {
|
|
|
518
541
|
return;
|
|
519
542
|
}
|
|
520
543
|
const entry = { child, stream: null, early: [] };
|
|
521
|
-
entry.opened =
|
|
522
|
-
.then((client) =>
|
|
544
|
+
entry.opened = pulse.acquire(child)
|
|
545
|
+
.then((client) => {
|
|
546
|
+
if (this.#audio.get(id) !== entry) throw Error("Audio owner closed");
|
|
547
|
+
return client.openPlayback({
|
|
523
548
|
rate, channels, name: String(name ?? "audio").slice(0, 200),
|
|
524
549
|
leadSeconds: name === "piano" ? 0.12 : undefined,
|
|
525
550
|
onRequest: (bytes) => send({ event: "request", bytes }),
|
|
526
551
|
onTiming: name === "piano" ? null : (timing) => send({ event: "timing", ...timing }),
|
|
527
552
|
onTimingError: (error) => send({ event: "timing-error", message: String(error?.message ?? error) }),
|
|
528
|
-
})
|
|
553
|
+
});
|
|
554
|
+
})
|
|
529
555
|
.then((stream) => {
|
|
530
556
|
if (this.#audio.get(id) !== entry) return stream.close();
|
|
531
557
|
entry.stream = stream;
|
|
532
558
|
for (const data of entry.early.splice(0)) stream.write(data);
|
|
533
559
|
send({ event: "ready" });
|
|
534
560
|
}, (error) => {
|
|
561
|
+
if (this.#audio.get(id) !== entry) return;
|
|
535
562
|
this.#audio.delete(id);
|
|
536
563
|
send({ event: "error", message: String(error?.message ?? error) });
|
|
537
564
|
});
|
|
@@ -539,6 +566,7 @@ export class RendererHost {
|
|
|
539
566
|
}
|
|
540
567
|
|
|
541
568
|
#closeAudio(child) {
|
|
569
|
+
pulse.release(child);
|
|
542
570
|
for (const [id, entry] of this.#audio) {
|
|
543
571
|
if (entry.child !== child) continue;
|
|
544
572
|
this.#audio.delete(id);
|
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
|
*
|
|
@@ -241,22 +242,27 @@ async function startScriptRewriter() {
|
|
|
241
242
|
|
|
242
243
|
let nextClipboardId=1;
|
|
243
244
|
const clipboardWrites=new Map();
|
|
244
|
-
function clipboardWrite(
|
|
245
|
+
function clipboardRead() { return clipboardWrite(undefined, 'clipboard-read'); }
|
|
246
|
+
function clipboardWrite(text, type = 'clipboard-write') {
|
|
245
247
|
const id=nextClipboardId++;
|
|
246
248
|
return new Promise((resolve,reject)=>{
|
|
247
249
|
const timer=setTimeout(()=>{clipboardWrites.delete(id);reject(Error('Clipboard timed out'));},5000);
|
|
248
250
|
clipboardWrites.set(id,{resolve,reject,timer});
|
|
249
|
-
process.send({type
|
|
251
|
+
process.send({type,id,text});
|
|
250
252
|
});
|
|
251
253
|
}
|
|
252
254
|
|
|
253
255
|
const renderer = await createPageRenderer({
|
|
256
|
+
renderMinInterval: renderMinInterval(process.argv.find(arg => arg.startsWith(RENDER_INTERVAL_FLAG))?.slice(RENDER_INTERVAL_FLAG.length)),
|
|
254
257
|
fetchResource,
|
|
255
258
|
rewriteScript,
|
|
256
259
|
requestNavigation: (url, download = null, fragmentFrom = null, post = null) => process.send({ type: "navigate", url, download, fragmentFrom, post }),
|
|
257
260
|
requestDownload: ({ url, name, data }) => process.send({ type: "download-data", url, name, data }),
|
|
258
261
|
pageFetch,
|
|
262
|
+
clipboardRead,
|
|
259
263
|
clipboardWrite,
|
|
264
|
+
clipboardReadInput:()=>clipboardWrite(undefined,'clipboard-read-input'),
|
|
265
|
+
clipboardWriteInput:text=>clipboardWrite(text,'clipboard-write-input'),
|
|
260
266
|
webSockets,
|
|
261
267
|
audio,
|
|
262
268
|
setCookie: (value) => process.send({ type: "cookie", value }),
|
|
@@ -281,11 +287,20 @@ const renderer = await createPageRenderer({
|
|
|
281
287
|
videoDecoder: wasmpeg,
|
|
282
288
|
});
|
|
283
289
|
await warmUp();
|
|
284
|
-
|
|
290
|
+
let allowHostJs = process.argv.includes(DANGEROUSLY_ALLOW_HOST_JS);
|
|
291
|
+
let hostReason = `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}`;
|
|
292
|
+
|
|
293
|
+
// qml handle begin: controller-selected local QML application process
|
|
294
|
+
if (process.argv.includes("--buninu-qml-host")) {
|
|
295
|
+
allowHostJs = true;
|
|
296
|
+
hostReason = "local QML application renderer";
|
|
297
|
+
}
|
|
298
|
+
// qml handle end
|
|
299
|
+
|
|
285
300
|
const seccomp = allowHostJs
|
|
286
|
-
? { installed: false, reason:
|
|
301
|
+
? { installed: false, reason: hostReason }
|
|
287
302
|
: installSeccomp();
|
|
288
|
-
const ses = allowHostJs ? { installed: false, reason:
|
|
303
|
+
const ses = allowHostJs ? { installed: false, reason: hostReason } : lockdownRealm();
|
|
289
304
|
renderer.setScripting(allowHostJs ? "host" : ses.installed ? "ses" : "off");
|
|
290
305
|
|
|
291
306
|
/** Hardens the JavaScript realm so page scripts can run in SES compartments. */
|
|
@@ -340,7 +355,7 @@ process.on("message", async (message) => {
|
|
|
340
355
|
if(message?.type==='clipboard-result') {
|
|
341
356
|
const pending=clipboardWrites.get(message.id);if(!pending)return;
|
|
342
357
|
clipboardWrites.delete(message.id);clearTimeout(pending.timer);
|
|
343
|
-
if(message.error)pending.reject(Error(String(message.error)));else pending.resolve();
|
|
358
|
+
if(message.error)pending.reject(Error(String(message.error)));else pending.resolve(message.text);
|
|
344
359
|
return;
|
|
345
360
|
}
|
|
346
361
|
|