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,83 @@
|
|
|
1
|
+
// Experimental Qt-style GLSL fragment subset -> SkSL. No app-specific shaders.
|
|
2
|
+
// This is not a general GLSL compiler: unsupported syntax is rejected by SkSL,
|
|
3
|
+
// and unsupported interfaces/preprocessor features fail before compilation.
|
|
4
|
+
export function translateFragment(source) {
|
|
5
|
+
if (/\b_qml_\w*/.test(String(source))) throw Error('Reserved shader adapter identifier');
|
|
6
|
+
source=String(source).replace(/\/\*[\s\S]*?\*\//g,' ').replace(/\/\/[^\n]*/g,'');
|
|
7
|
+
source=source.replace(/^\s*#version[^\n]*$/gm,'');
|
|
8
|
+
if(/^\s*#/m.test(source))throw Error('Shader preprocessor directives are not implemented');
|
|
9
|
+
if(/\b(?:gl_FragDepth|discard|samplerCube|sampler3D|image2D)\b/.test(source))throw Error('Unsupported fragment shader feature');
|
|
10
|
+
source=source.replace(/layout\s*\([^)]*\)\s*/g,'');
|
|
11
|
+
source=source.replace(/\bprecision\s+(?:lowp|mediump|highp)\s+\w+\s*;/g,'').replace(/\b(?:highp|mediump|lowp)\s+/g,'');
|
|
12
|
+
const blocks=[];
|
|
13
|
+
for(const m of source.matchAll(/struct\s+(\w+)\s*\{([^{}]*)\}\s*;/g)) {
|
|
14
|
+
const matches=[...source.matchAll(new RegExp('uniform\\s+'+m[1]+'\\s+(\\w+)\\s*;','g'))];
|
|
15
|
+
if(matches.length===1)blocks.push({declaration:m[0],body:m[2],uniform:matches[0][0],instance:matches[0][1]});
|
|
16
|
+
}
|
|
17
|
+
for(const block of blocks){
|
|
18
|
+
source=source.replace(block.declaration,'').replace(block.uniform,block.body.split(';').filter(s=>s.trim()).map(s=>'uniform '+s.trim()+';').join('\n'));
|
|
19
|
+
source=source.replace(new RegExp('\\b'+block.instance+'\\s*\\.\\s*','g'),'');
|
|
20
|
+
}
|
|
21
|
+
const samplers=[];
|
|
22
|
+
source=source.replace(/uniform\s+sampler2D\s+(\w+)\s*;/g,(_,name)=>{
|
|
23
|
+
samplers.push(name);
|
|
24
|
+
return `uniform shader ${name}; uniform float2 _qml_size_${name};`;
|
|
25
|
+
});
|
|
26
|
+
source=source.replace(/uniform\s+\w+\s*\{([^{}]*)\}\s*;/g,(_,body)=>body.split(';').filter(s=>s.trim()).map(s=>'uniform '+s.trim()+';').join('\n'));
|
|
27
|
+
let output=null,coordinates=false;
|
|
28
|
+
source=source.replace(/\bin\s+vec2\s+qt_TexCoord0\s*;/g,()=>{coordinates=true;return 'float2 qt_TexCoord0;';});
|
|
29
|
+
source=source.replace(/\bout\s+vec4\s+(\w+)\s*;/g,(_,name)=>{
|
|
30
|
+
if(output)throw Error('Multiple fragment outputs are not supported');
|
|
31
|
+
output=name;return `float4 ${name};`;
|
|
32
|
+
});
|
|
33
|
+
if(!output)throw Error('Fragment shader requires a vec4 output');
|
|
34
|
+
if(/\b(?:in|out)\s+\w+\s+\w+\s*;/.test(source))throw Error('Unsupported fragment varying');
|
|
35
|
+
const types={vec2:'float2',vec3:'float3',vec4:'float4',ivec2:'int2',ivec3:'int3',ivec4:'int4',bvec2:'bool2',bvec3:'bool3',bvec4:'bool4',mat2:'float2x2',mat3:'float3x3',mat4:'float4x4'};
|
|
36
|
+
source=source.replace(/\b(?:[ib]?vec[234]|mat[234])\b/g,type=>types[type]);
|
|
37
|
+
for (const call of source.matchAll(/\btextureSize\s*\(\s*\w+\s*,([^)]*)\)/g)) if (call[1].trim()!=='0') throw Error('Only base-level textureSize is supported');
|
|
38
|
+
source=source.replace(/\b(texture|textureSize)\s*\(\s*(\w+)\s*,/g,(_,operation,name)=>{
|
|
39
|
+
if(!samplers.includes(name))throw Error('Sampler expressions are not supported');
|
|
40
|
+
return `_qml_${operation}_${name}(`;
|
|
41
|
+
});
|
|
42
|
+
if(!/\bvoid\s+main\s*\(\s*(?:void)?\s*\)/.test(source))throw Error('Missing fragment main');
|
|
43
|
+
source=source.replace(/\bvoid\s+main\s*\(\s*(?:void)?\s*\)/,'void _qml_fragment()');
|
|
44
|
+
source=source.replace(/\bround\s*\(/g,'_qml_round(');
|
|
45
|
+
const rounding=['float','float2','float3','float4'].map(type=>`${type} _qml_round(${type} x) {return sign(x)*floor(abs(x)+0.5);}`).join('\n');
|
|
46
|
+
const helpers=rounding+'\n'+samplers.map(name=>`float4 _qml_texture_${name}(float2 uv) {return ${name}.eval(uv * _qml_size_${name});}\nint2 _qml_textureSize_${name}(int lod) {return int2(_qml_size_${name});}`).join('\n');
|
|
47
|
+
// Functions must follow their uniforms and precede their call sites.
|
|
48
|
+
const mainAt=source.search(/\b(?:void|float\w*|int\w*|bool\w*)\s+\w+\s*\(/);
|
|
49
|
+
source=source.slice(0,mainAt)+helpers+'\n'+source.slice(mainAt);
|
|
50
|
+
return {samplers,source:`uniform float2 _qml_viewport;\n${source}\nhalf4 main(float2 p) {${coordinates?'qt_TexCoord0 = p / _qml_viewport;':''}${output}=float4(0);_qml_fragment();return half4(${output});}`};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function compileFragment(kit, source) {
|
|
54
|
+
const translated=translateFragment(source);
|
|
55
|
+
let diagnostic='';
|
|
56
|
+
const effect=kit.RuntimeEffect.Make(translated.source,message=>{diagnostic+=message;});
|
|
57
|
+
if(!effect)throw Error('Software fragment compilation failed: '+diagnostic);
|
|
58
|
+
return {...translated,effect};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Reuse child shaders from the renderer's existing CanvasKit instance. The
|
|
62
|
+
// caller owns the returned shader, effect and children; no pixel readback here.
|
|
63
|
+
export function makeFragmentShader(compiled, {width,height,uniforms={},textures={}}) {
|
|
64
|
+
const values=new Float32Array(compiled.effect.getUniformFloatCount());
|
|
65
|
+
// CanvasKit converts integer uniforms from numeric float slots internally.
|
|
66
|
+
const supplied={qt_Opacity:1,qt_Matrix:[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],...uniforms,_qml_viewport:[width,height]};
|
|
67
|
+
const children=compiled.samplers.map(name=>{
|
|
68
|
+
const texture=textures[name];
|
|
69
|
+
if(!texture?.shader || !(texture.width>0) || !(texture.height>0))throw Error('Missing shader texture: '+name);
|
|
70
|
+
supplied['_qml_size_'+name]=[texture.width,texture.height];
|
|
71
|
+
return texture.shader;
|
|
72
|
+
});
|
|
73
|
+
for(let i=0;i<compiled.effect.getUniformCount();i++){
|
|
74
|
+
const name=compiled.effect.getUniformName(i),info=compiled.effect.getUniform(i);
|
|
75
|
+
const count=info.columns*info.rows;
|
|
76
|
+
const value=supplied[name] ?? Array(count).fill(0);
|
|
77
|
+
const parts=typeof value==='number' ? [value] : Array.from(value);
|
|
78
|
+
if(parts.length!==count)throw Error('Wrong shader uniform size: '+name);
|
|
79
|
+
if(parts.some(v=>!Number.isFinite(v)))throw Error('Nonfinite shader uniform: '+name);
|
|
80
|
+
values.set(parts,info.slot);
|
|
81
|
+
}
|
|
82
|
+
return compiled.effect.makeShaderWithChildren(values,children);
|
|
83
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {inflateSync} from 'node:zlib';
|
|
2
|
+
|
|
3
|
+
// QSB v9 stores QDataStream shader keys followed by length-prefixed source and
|
|
4
|
+
// entrypoint QByteArrays. Reflection is intentionally opaque; validate complete
|
|
5
|
+
// GLSL records rather than depending on its changing private schema.
|
|
6
|
+
export function fragmentSource(bytes) {
|
|
7
|
+
bytes=Buffer.from(bytes);
|
|
8
|
+
if(bytes.subarray(0,256).toString().trimStart().startsWith('#version'))return bytes.toString();
|
|
9
|
+
const limit=8*1024*1024;
|
|
10
|
+
if(bytes.length<8||bytes.readUInt32BE(0)>limit)throw Error('Invalid or oversized QSB');
|
|
11
|
+
const data=inflateSync(bytes.subarray(4),{maxOutputLength:limit});
|
|
12
|
+
if(data.length!==bytes.readUInt32BE(0)||data.length<8||data.readUInt32BE(0)!==9||data.readUInt32BE(4)!==4)throw Error('Only QSB v9 fragment packages are supported');
|
|
13
|
+
const candidates=[];
|
|
14
|
+
for(let p=20;p<data.length;p++) {
|
|
15
|
+
if(data[p]!==35||data.subarray(p,p+8).toString()!=='#version')continue;
|
|
16
|
+
const size=data.readUInt32BE(p-4),end=p+size;
|
|
17
|
+
if(data.readUInt32BE(p-20)!==1||data.readUInt32BE(p-8)!==0||size>limit||end+8>data.length)continue;
|
|
18
|
+
const version=data.readUInt32BE(p-16),flags=data.readUInt32BE(p-12);
|
|
19
|
+
if(flags!==0||version<150||version>460)continue;
|
|
20
|
+
if(data.readUInt32BE(end)!==4||data.subarray(end+4,end+8).toString()!=='main')continue;
|
|
21
|
+
const source=data.subarray(p,end).toString();
|
|
22
|
+
if(!source.startsWith(`#version ${version}`)||source.includes('\0'))continue;
|
|
23
|
+
candidates.push({version,source});p=end+7;
|
|
24
|
+
}
|
|
25
|
+
if(!candidates.length)throw Error('QSB has no supported standard desktop GLSL fragment');
|
|
26
|
+
return candidates.sort((a,b)=>a.version-b.version)[0].source;
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {readFileSync} from 'node:fs';
|
|
2
|
+
import {fragmentSource} from './qsb.js';
|
|
3
|
+
import {compileFragment,makeFragmentShader} from './fragment.js';
|
|
4
|
+
|
|
5
|
+
// Local-QML capability. Native handles never enter the document realm.
|
|
6
|
+
export function createShaderRenderer({kit,unwrap,valid,resource,paint,track}) {
|
|
7
|
+
function node(value) {const n=unwrap(value)??value;if(!valid(n))throw Error('Invalid QML shader element');return n;}
|
|
8
|
+
return url=>{
|
|
9
|
+
const address=new URL(String(url));
|
|
10
|
+
if(address.protocol!=='file:')throw Error('ShaderEffect requires a local shader file');
|
|
11
|
+
const compiled=compileFragment(kit,fragmentSource(readFileSync(address)));
|
|
12
|
+
let disposed=false;
|
|
13
|
+
const stop=track(()=>{disposed=true;compiled.effect.delete();});
|
|
14
|
+
const names=Array.from({length:compiled.effect.getUniformCount()},(_,i)=>compiled.effect.getUniformName(i)).filter(n=>!n.startsWith('_qml_')&&!['qt_Matrix','qt_Opacity'].includes(n));
|
|
15
|
+
return {
|
|
16
|
+
uniforms:names,samplers:compiled.samplers,
|
|
17
|
+
dispose:stop,
|
|
18
|
+
draw(target,uniforms,inputs,smooth=true) {
|
|
19
|
+
if(disposed)throw Error('ShaderEffect is disposed');
|
|
20
|
+
target=node(target);
|
|
21
|
+
if(target.tagName!=='CANVAS')throw Error('ShaderEffect target must be a canvas');
|
|
22
|
+
const children=[],textures={};let shader,p;
|
|
23
|
+
try {
|
|
24
|
+
for(const name of compiled.samplers){
|
|
25
|
+
if(!inputs[name]?.element)return false;
|
|
26
|
+
const r=resource(node(inputs[name].element));
|
|
27
|
+
if(!r?.image)return false;
|
|
28
|
+
const child=r.image.makeShaderOptions(kit.TileMode.Clamp,kit.TileMode.Clamp,inputs[name].smooth?kit.FilterMode.Linear:kit.FilterMode.Nearest,kit.MipmapMode.None);
|
|
29
|
+
children.push(child);textures[name]={shader:child,width:r.width,height:r.height};
|
|
30
|
+
}
|
|
31
|
+
shader=makeFragmentShader(compiled,{width:target.width,height:target.height,uniforms,textures});
|
|
32
|
+
p=new kit.Paint();p.setShader(shader);p.setBlendMode(kit.BlendMode.Src);
|
|
33
|
+
paint(target,canvas=>canvas.drawRect(kit.XYWHRect(0,0,target.width,target.height),p));
|
|
34
|
+
return true;
|
|
35
|
+
}finally{p?.delete();shader?.delete();for(const child of children)child.delete();}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Serialized local QML adapter. Rendering remains an ordinary HTML canvas.
|
|
2
|
+
export function createQmlShaderEffects(api) {
|
|
3
|
+
const {effect,untrack,elements,host,cleanup}=api;
|
|
4
|
+
function make(v,type,element){
|
|
5
|
+
if(type!=='ShaderEffect')return;
|
|
6
|
+
Object.assign(v,{fragmentShader:'',vertexShader:'',status:0,log:'',blending:true,cullMode:0});
|
|
7
|
+
const canvas=document.createElement('canvas');
|
|
8
|
+
canvas.style.cssText='position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none';
|
|
9
|
+
element.appendChild(canvas);
|
|
10
|
+
let program=null,source=null,frame,disposed=false;
|
|
11
|
+
const fail=e=>{v.status=2;v.log=String(e.message||e);};
|
|
12
|
+
effect(()=>{
|
|
13
|
+
const url=v.fragmentShader,vertex=v.vertexShader;
|
|
14
|
+
untrack(()=>{
|
|
15
|
+
if(url===source&&!vertex)return;
|
|
16
|
+
source=url;program?.dispose();program=null;v.status=0;v.log='';
|
|
17
|
+
if(!url)return;
|
|
18
|
+
try{
|
|
19
|
+
if(vertex)throw Error('Custom vertex shaders are not implemented');
|
|
20
|
+
if(!host?.shader)throw Error('ShaderEffect requires a local QML renderer');
|
|
21
|
+
program=host.shader(api.resolve(v,url));v.status=1;
|
|
22
|
+
}catch(e){fail(e);}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
effect(()=>{
|
|
26
|
+
v.status;v.fragmentShader;
|
|
27
|
+
const width=Math.ceil(v.width),height=Math.ceil(v.height),smooth=v.smooth!==false;
|
|
28
|
+
const uniforms={},inputs={};
|
|
29
|
+
if(program){
|
|
30
|
+
for(const name of program.uniforms){
|
|
31
|
+
const value=v[name];
|
|
32
|
+
uniforms[name]=value&&typeof value==='object' ? ('r' in value?[value.r,value.g,value.b,value.a]: 'x' in value?['x','y','z','w'].filter(k=>k in value).map(k=>value[k]):Array.from(value)):value;
|
|
33
|
+
}
|
|
34
|
+
for(const name of program.samplers){
|
|
35
|
+
const value=v[name];
|
|
36
|
+
if(!value)continue;
|
|
37
|
+
value.status;value.source;
|
|
38
|
+
const element=elements.get(value);
|
|
39
|
+
inputs[name]=element?.querySelector('img,canvas');
|
|
40
|
+
if(value.sourceItem!==undefined)inputs[name]=api.texture(value);
|
|
41
|
+
inputs[name]={element:inputs[name],smooth:value.smooth!==false};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
untrack(()=>{
|
|
45
|
+
cancelAnimationFrame(frame);
|
|
46
|
+
if(!program||!(width>0&&height>0))return;
|
|
47
|
+
if(width>8192||height>8192||width*height>16777216){fail(Error('ShaderEffect size limit'));return;}
|
|
48
|
+
frame=requestAnimationFrame(()=>{
|
|
49
|
+
if(disposed||!program)return;
|
|
50
|
+
try{
|
|
51
|
+
if(canvas.width!==width)canvas.width=width;
|
|
52
|
+
if(canvas.height!==height)canvas.height=height;
|
|
53
|
+
program.draw(canvas,uniforms,inputs,smooth);
|
|
54
|
+
}catch(e){fail(e);}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
cleanup(()=>{disposed=true;cancelAnimationFrame(frame);program?.dispose();canvas.remove();});
|
|
59
|
+
}
|
|
60
|
+
return {make};
|
|
61
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// Solid QML paths keep the existing SVG DOM path. A gradient path uses a normal
|
|
2
|
+
// page canvas because this browser's inline-SVG painter has no gradient support.
|
|
3
|
+
// No image encoding, readback or extra CanvasKit instance is involved.
|
|
4
|
+
export function createQmlShapes(api) {
|
|
5
|
+
const { elements, parents, owners, children, cssColor } = api;
|
|
6
|
+
const roots = new WeakMap(),
|
|
7
|
+
canvases = new WeakMap();
|
|
8
|
+
function multiline(paths) {
|
|
9
|
+
return (paths || []).map(points => points.map((p,i) => {
|
|
10
|
+
const x = Number(p.x ?? p[0]), y = Number(p.y ?? p[1]);
|
|
11
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) throw Error('Invalid PathMultiline point');
|
|
12
|
+
return `${i ? 'L' : 'M'}${x} ${y}`;
|
|
13
|
+
}).join(' ')).join(' ');
|
|
14
|
+
}
|
|
15
|
+
function path(ctx, source) {
|
|
16
|
+
const tokens =
|
|
17
|
+
String(source).match(
|
|
18
|
+
/[a-zA-Z]|[-+]?(?:\d*\.\d+|\d+\.?\d*)(?:[eE][-+]?\d+)?/g,
|
|
19
|
+
) || [];
|
|
20
|
+
let i = 0,
|
|
21
|
+
x = 0,
|
|
22
|
+
y = 0,
|
|
23
|
+
sx = 0,
|
|
24
|
+
sy = 0,
|
|
25
|
+
cx = 0,
|
|
26
|
+
cy = 0,
|
|
27
|
+
previous = '',
|
|
28
|
+
command = '';
|
|
29
|
+
const number = () => {
|
|
30
|
+
const n = Number(tokens[i++]);
|
|
31
|
+
if (!Number.isFinite(n)) throw Error('Invalid QML SVG path');
|
|
32
|
+
return n;
|
|
33
|
+
};
|
|
34
|
+
const point = (relative) => {
|
|
35
|
+
const a = number(),
|
|
36
|
+
b = number();
|
|
37
|
+
return [a + (relative ? x : 0), b + (relative ? y : 0)];
|
|
38
|
+
};
|
|
39
|
+
while (i < tokens.length) {
|
|
40
|
+
if (/^[a-zA-Z]$/.test(tokens[i])) command = tokens[i++];
|
|
41
|
+
if (!command) throw Error('Missing SVG path command');
|
|
42
|
+
const relative = command === command.toLowerCase(),
|
|
43
|
+
kind = command.toUpperCase();
|
|
44
|
+
if (kind === 'Z') {
|
|
45
|
+
ctx.closePath();
|
|
46
|
+
x = sx;
|
|
47
|
+
y = sy;
|
|
48
|
+
previous = kind;
|
|
49
|
+
command = '';
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (kind === 'M' || kind === 'L' || kind === 'T') {
|
|
53
|
+
const [nx, ny] = point(relative);
|
|
54
|
+
if (kind === 'M') {
|
|
55
|
+
ctx.moveTo(nx, ny);
|
|
56
|
+
sx = nx;
|
|
57
|
+
sy = ny;
|
|
58
|
+
command = relative ? 'l' : 'L';
|
|
59
|
+
} else if (kind === 'T') {
|
|
60
|
+
const a = ['Q', 'T'].includes(previous) ? 2 * x - cx : x,
|
|
61
|
+
b = ['Q', 'T'].includes(previous) ? 2 * y - cy : y;
|
|
62
|
+
ctx.quadraticCurveTo(a, b, nx, ny);
|
|
63
|
+
cx = a;
|
|
64
|
+
cy = b;
|
|
65
|
+
} else ctx.lineTo(nx, ny);
|
|
66
|
+
x = nx;
|
|
67
|
+
y = ny;
|
|
68
|
+
} else if (kind === 'H') {
|
|
69
|
+
x = number() + (relative ? x : 0);
|
|
70
|
+
ctx.lineTo(x, y);
|
|
71
|
+
} else if (kind === 'V') {
|
|
72
|
+
y = number() + (relative ? y : 0);
|
|
73
|
+
ctx.lineTo(x, y);
|
|
74
|
+
} else if (kind === 'C') {
|
|
75
|
+
const a = point(relative),
|
|
76
|
+
b = point(relative),
|
|
77
|
+
end = point(relative);
|
|
78
|
+
ctx.bezierCurveTo(...a, ...b, ...end);
|
|
79
|
+
[cx, cy] = b;
|
|
80
|
+
[x, y] = end;
|
|
81
|
+
} else if (kind === 'S') {
|
|
82
|
+
const a = ['C', 'S'].includes(previous)
|
|
83
|
+
? [2 * x - cx, 2 * y - cy]
|
|
84
|
+
: [x, y],
|
|
85
|
+
b = point(relative),
|
|
86
|
+
end = point(relative);
|
|
87
|
+
ctx.bezierCurveTo(...a, ...b, ...end);
|
|
88
|
+
[cx, cy] = b;
|
|
89
|
+
[x, y] = end;
|
|
90
|
+
} else if (kind === 'Q') {
|
|
91
|
+
const a = point(relative),
|
|
92
|
+
end = point(relative);
|
|
93
|
+
ctx.quadraticCurveTo(...a, ...end);
|
|
94
|
+
[cx, cy] = a;
|
|
95
|
+
[x, y] = end;
|
|
96
|
+
} else if (kind === 'A') {
|
|
97
|
+
let rx = Math.abs(number()),
|
|
98
|
+
ry = Math.abs(number());
|
|
99
|
+
const phi = (number() * Math.PI) / 180,
|
|
100
|
+
large = number() !== 0,
|
|
101
|
+
sweep = number() !== 0,
|
|
102
|
+
[nx, ny] = point(relative);
|
|
103
|
+
if (!rx || !ry) ctx.lineTo(nx, ny);
|
|
104
|
+
else if (x !== nx || y !== ny) {
|
|
105
|
+
const cos = Math.cos(phi),
|
|
106
|
+
sin = Math.sin(phi),
|
|
107
|
+
dx = (x - nx) / 2,
|
|
108
|
+
dy = (y - ny) / 2,
|
|
109
|
+
px = cos * dx + sin * dy,
|
|
110
|
+
py = -sin * dx + cos * dy;
|
|
111
|
+
const scale = (px * px) / (rx * rx) + (py * py) / (ry * ry);
|
|
112
|
+
if (scale > 1) {
|
|
113
|
+
rx *= Math.sqrt(scale);
|
|
114
|
+
ry *= Math.sqrt(scale);
|
|
115
|
+
}
|
|
116
|
+
const numerator = Math.max(
|
|
117
|
+
0,
|
|
118
|
+
rx * rx * ry * ry - rx * rx * py * py - ry * ry * px * px,
|
|
119
|
+
),
|
|
120
|
+
denominator = rx * rx * py * py + ry * ry * px * px;
|
|
121
|
+
const factor =
|
|
122
|
+
(large === sweep ? -1 : 1) * Math.sqrt(numerator / denominator),
|
|
123
|
+
ax = (factor * rx * py) / ry,
|
|
124
|
+
ay = (-factor * ry * px) / rx;
|
|
125
|
+
const centerX = cos * ax - sin * ay + (x + nx) / 2,
|
|
126
|
+
centerY = sin * ax + cos * ay + (y + ny) / 2;
|
|
127
|
+
const start = Math.atan2((py - ay) / ry, (px - ax) / rx),
|
|
128
|
+
end = Math.atan2((-py - ay) / ry, (-px - ax) / rx);
|
|
129
|
+
ctx.ellipse(centerX, centerY, rx, ry, phi, start, end, !sweep);
|
|
130
|
+
}
|
|
131
|
+
x = nx;
|
|
132
|
+
y = ny;
|
|
133
|
+
} else throw Error('Unsupported QML SVG command: ' + command);
|
|
134
|
+
previous = kind;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function make(v, type, element, parent) {
|
|
138
|
+
if (type === 'Shape') {
|
|
139
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
140
|
+
svg.style.cssText =
|
|
141
|
+
'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none';
|
|
142
|
+
while (element.firstChild) svg.appendChild(element.firstChild);
|
|
143
|
+
element.appendChild(svg);
|
|
144
|
+
roots.set(v, svg);
|
|
145
|
+
} else if (type === 'ShapePath' && roots.has(parent))
|
|
146
|
+
roots.get(parent).appendChild(element);
|
|
147
|
+
}
|
|
148
|
+
function paint(v, type) {
|
|
149
|
+
if (type === 'Shape') {
|
|
150
|
+
const svg = roots.get(v);
|
|
151
|
+
svg.setAttribute('width', String(v.width));
|
|
152
|
+
svg.setAttribute('height', String(v.height));
|
|
153
|
+
svg.setAttribute(
|
|
154
|
+
'viewBox',
|
|
155
|
+
'0 0 ' + (v.width || 1) + ' ' + (v.height || 1),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (type !== 'ShapePath') return;
|
|
159
|
+
const element = elements.get(v),
|
|
160
|
+
root = parents.get(v),
|
|
161
|
+
gradient = v.fillGradient,
|
|
162
|
+
fillItem = v.fillItem;
|
|
163
|
+
if (!gradient && !fillItem) {
|
|
164
|
+
const old = canvases.get(v);
|
|
165
|
+
if (old) old.style.display = 'none';
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (gradient && owners.get(gradient)?.type !== 'LinearGradient')
|
|
169
|
+
throw Error('Unsupported QML shape gradient');
|
|
170
|
+
const host = elements.get(root);
|
|
171
|
+
let canvas = canvases.get(v);
|
|
172
|
+
if (!canvas) {
|
|
173
|
+
canvas = document.createElement('canvas');
|
|
174
|
+
canvas.setAttribute('data-qml-gradient', '');
|
|
175
|
+
canvas.style.cssText =
|
|
176
|
+
'position:absolute;left:0;top:0;pointer-events:none';
|
|
177
|
+
host.appendChild(canvas);
|
|
178
|
+
canvases.set(v, canvas);
|
|
179
|
+
api.cleanup(() => {
|
|
180
|
+
canvas.width = 1;
|
|
181
|
+
canvas.height = 1;
|
|
182
|
+
canvas.remove();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
canvas.style.display = 'block';
|
|
186
|
+
element.style.display = 'none';
|
|
187
|
+
const width = Math.max(1, Math.ceil(root.width)),
|
|
188
|
+
height = Math.max(1, Math.ceil(root.height));
|
|
189
|
+
if (canvas.width !== width) canvas.width = width;
|
|
190
|
+
if (canvas.height !== height) canvas.height = height;
|
|
191
|
+
canvas.style.width = root.width + 'px';
|
|
192
|
+
canvas.style.height = root.height + 'px';
|
|
193
|
+
const context = canvas.getContext('2d');
|
|
194
|
+
context.clearRect(0, 0, width, height);
|
|
195
|
+
context.beginPath();
|
|
196
|
+
for (const child of children(v)) {
|
|
197
|
+
const type = owners.get(child)?.type;
|
|
198
|
+
if (type === 'PathSvg') path(context, child.path);
|
|
199
|
+
else if (type === 'PathMultiline') path(context, multiline(child.paths));
|
|
200
|
+
else if (type === 'PathRectangle') {
|
|
201
|
+
const radii = [
|
|
202
|
+
'topLeftRadius',
|
|
203
|
+
'topRightRadius',
|
|
204
|
+
'bottomRightRadius',
|
|
205
|
+
'bottomLeftRadius',
|
|
206
|
+
].map((key) => child[key] ?? child.radius ?? 0);
|
|
207
|
+
// CanvasKit's 2D shim lacks roundRect; build the same path with arcTo.
|
|
208
|
+
const x = child.x, y = child.y, w = child.width, h = child.height;
|
|
209
|
+
const r = radii.map(value => Math.max(0, Number(value) || 0));
|
|
210
|
+
const factor = Math.min(1, w / (r[0] + r[1] || 1), w / (r[2] + r[3] || 1),
|
|
211
|
+
h / (r[0] + r[3] || 1), h / (r[1] + r[2] || 1));
|
|
212
|
+
for (let i = 0; i < 4; i++) r[i] *= Math.max(0, factor);
|
|
213
|
+
context.moveTo(x + r[0], y);
|
|
214
|
+
context.lineTo(x + w - r[1], y);context.arcTo(x + w, y, x + w, y + r[1], r[1]);
|
|
215
|
+
context.lineTo(x + w, y + h - r[2]);context.arcTo(x + w, y + h, x + w - r[2], y + h, r[2]);
|
|
216
|
+
context.lineTo(x + r[3], y + h);context.arcTo(x, y + h, x, y + h - r[3], r[3]);
|
|
217
|
+
context.lineTo(x, y + r[0]);context.arcTo(x, y, x + r[0], y, r[0]);
|
|
218
|
+
context.closePath();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (fillItem) {
|
|
222
|
+
const bitmap = api.texture(fillItem);
|
|
223
|
+
if (bitmap) {
|
|
224
|
+
context.save();
|
|
225
|
+
context.clip(v.fillRule === 0 ? 'evenodd' : 'nonzero');
|
|
226
|
+
context.drawImage(bitmap, 0, 0, width, height);
|
|
227
|
+
context.restore();
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
const fill = context.createLinearGradient(gradient.x1, gradient.y1, gradient.x2, gradient.y2);
|
|
231
|
+
for (const stop of children(gradient))
|
|
232
|
+
fill.addColorStop(Math.max(0, Math.min(1, stop.position)), cssColor(stop.color));
|
|
233
|
+
context.fillStyle = fill;
|
|
234
|
+
context.fill(v.fillRule === 0 ? 'evenodd' : 'nonzero');
|
|
235
|
+
}
|
|
236
|
+
if (v.strokeWidth > 0) {
|
|
237
|
+
context.strokeStyle = cssColor(v.strokeColor);
|
|
238
|
+
context.lineWidth = v.strokeWidth;
|
|
239
|
+
context.lineCap =
|
|
240
|
+
v.capStyle === 32 ? 'round' : v.capStyle === 16 ? 'square' : 'butt';
|
|
241
|
+
context.lineJoin =
|
|
242
|
+
v.joinStyle === 128 ? 'round' : v.joinStyle === 64 ? 'bevel' : 'miter';
|
|
243
|
+
context.setLineDash(
|
|
244
|
+
v.strokeStyle === 2 ? v.dashPattern.map((n) => n * v.strokeWidth) : [],
|
|
245
|
+
);
|
|
246
|
+
context.lineDashOffset = v.dashOffset * v.strokeWidth;
|
|
247
|
+
context.stroke();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { make, paint, multiline };
|
|
251
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// QML Shortcut adapter over the existing DOM keyboard events.
|
|
2
|
+
export function createQmlShortcuts(api) {
|
|
3
|
+
const entries = new Set();
|
|
4
|
+
const names = {escape:'Escape',esc:'Escape',space:'Space',tab:'Tab',backtab:'Backtab',return:'Return',enter:'Enter',left:'Left',right:'Right',up:'Up',down:'Down',home:'Home',end:'End',pgup:'PgUp',pageup:'PgUp',pgdown:'PgDown',pagedown:'PgDown',delete:'Del',del:'Del',insert:'Ins',ins:'Ins',backspace:'Backspace'};
|
|
5
|
+
const keyName = key => names[String(key).toLowerCase()] || (/^f(?:[1-9]|[12]\d|3[0-5])$/i.test(key) ? key.toUpperCase() : [...String(key)].length===1 ? String(key).toUpperCase() : '');
|
|
6
|
+
function parse(value) {
|
|
7
|
+
if (typeof value !== 'string' || !value.trim()) return [];
|
|
8
|
+
const strokes = value.trim()===',' ? [','] : value.split(/,\s*/);
|
|
9
|
+
if (strokes.length>4) return [];
|
|
10
|
+
const result=[];
|
|
11
|
+
for(const stroke of strokes){
|
|
12
|
+
let rest=stroke.trim(), mods=[];
|
|
13
|
+
while(/^(ctrl|alt|shift|meta)\+/i.test(rest)){
|
|
14
|
+
const at=rest.indexOf('+');mods.push(rest.slice(0,at).toLowerCase());rest=rest.slice(at+1);
|
|
15
|
+
}
|
|
16
|
+
const key=keyName(rest);if(!key)return [];
|
|
17
|
+
result.push({key,mods:new Set(mods)});
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
const text = value => parse(value).map(({key,mods})=>['ctrl','alt','shift','meta'].filter(m=>mods.has(m)).map(m=>m[0].toUpperCase()+m.slice(1)).concat(key).join('+')).join(', ');
|
|
22
|
+
function matches(stroke,event){
|
|
23
|
+
const key=event.key===' ' ? 'Space' : event.key==='Enter' ? 'Return' : event.key?.replace(/^Arrow/,'');
|
|
24
|
+
if(stroke.key!==keyName(key) && !(stroke.key==='Enter' && key==='Return'))return false;
|
|
25
|
+
for(const name of ['ctrl','alt','meta'])if(stroke.mods.has(name)!==!!event[name+'Key'])return false;
|
|
26
|
+
// Shift is intrinsic to printable punctuation such as '+' and '?'.
|
|
27
|
+
if(stroke.mods.has('shift')!==!!event.shiftKey && !(event.shiftKey && !stroke.mods.has('shift') && key?.length===1 && !/[a-z0-9]/i.test(key)))return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
function keydown(event){
|
|
31
|
+
if(event.defaultPrevented || event.isComposing)return;
|
|
32
|
+
const complete=[], partial=[];
|
|
33
|
+
for(const entry of entries){
|
|
34
|
+
const v=entry.v, window=v.Window?.window;
|
|
35
|
+
if(!v.enabled || (!v.autoRepeat && event.repeat) || (window && (window.visible===false || (v.context!==2 && api.activeWindow() && api.activeWindow()!==window)))){entry.pending=[];continue;}
|
|
36
|
+
const sequences=(v.sequences?.length?v.sequences:[v.sequence]).map(parse).filter(s=>s.length);
|
|
37
|
+
const candidates=entry.pending.length && Date.now()-entry.time<1000 ? entry.pending : sequences.map(s=>({s,at:0}));
|
|
38
|
+
entry.pending=[];
|
|
39
|
+
for(const candidate of candidates){
|
|
40
|
+
if(!matches(candidate.s[candidate.at],event))continue;
|
|
41
|
+
if(candidate.at+1===candidate.s.length)complete.push(entry);
|
|
42
|
+
else entry.pending.push({s:candidate.s,at:candidate.at+1});
|
|
43
|
+
}
|
|
44
|
+
if(entry.pending.length){entry.time=Date.now();partial.push(entry);}
|
|
45
|
+
}
|
|
46
|
+
if(!complete.length&&!partial.length)return;
|
|
47
|
+
event.preventDefault();event.stopImmediatePropagation();
|
|
48
|
+
const unique=[...new Set(complete)];
|
|
49
|
+
if(unique.length){
|
|
50
|
+
for(const entry of entries)entry.pending=[];
|
|
51
|
+
for(const entry of unique)api.emit(entry.v, unique.length>1||partial.length ? 'onActivatedAmbiguously':'onActivated');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function make(v,type){
|
|
55
|
+
if(type!=='Shortcut')return;
|
|
56
|
+
Object.assign(v,{sequence:'',sequences:[],autoRepeat:true,context:1});
|
|
57
|
+
Object.defineProperty(v,'portableText',{get:()=>text(v.sequences?.length?v.sequences[0]:v.sequence)});
|
|
58
|
+
Object.defineProperty(v,'nativeText',{get:()=>v.portableText});
|
|
59
|
+
const entry={v,pending:[],time:0};
|
|
60
|
+
if(!entries.size)document.addEventListener('keydown',keydown,true);
|
|
61
|
+
entries.add(entry);
|
|
62
|
+
api.cleanup(()=>{entries.delete(entry);if(!entries.size)document.removeEventListener('keydown',keydown,true);});
|
|
63
|
+
}
|
|
64
|
+
return {make};
|
|
65
|
+
}
|
package/lib/qml/text.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Qt Text's markup is content, not generated QML HTML. Build an inert, restricted
|
|
2
|
+
// DOM fragment; embedded scripts, handlers and external resource tags are never
|
|
3
|
+
// transferred to the live document. Rendering stays in the browser's CSS engine.
|
|
4
|
+
export function createQmlText(document, markdown) {
|
|
5
|
+
const cache = new WeakMap();
|
|
6
|
+
const tags = new Set(
|
|
7
|
+
'a b strong i em u s strike sub sup span font br p div pre code ul ol li h1 h2 h3 h4 h5 h6 blockquote table thead tbody tr td th hr'.split(
|
|
8
|
+
' ',
|
|
9
|
+
),
|
|
10
|
+
);
|
|
11
|
+
const blocks = new Set(
|
|
12
|
+
'p div pre li h1 h2 h3 h4 h5 h6 blockquote tr'.split(' '),
|
|
13
|
+
);
|
|
14
|
+
function copy(source, target, collapse = false, pre = false) {
|
|
15
|
+
for (const child of source.childNodes) {
|
|
16
|
+
if (child.nodeType === 3) {
|
|
17
|
+
// Markdown emits formatting newlines between block tags. They are not
|
|
18
|
+
// extra text lines, and must not inflate a Text item's implicitHeight.
|
|
19
|
+
if (/^\s*$/.test(child.textContent) &&
|
|
20
|
+
(blocks.has(child.previousSibling?.localName) || blocks.has(child.nextSibling?.localName))) continue;
|
|
21
|
+
const text = collapse && !pre ? child.textContent.replace(/[ \t\r\n\f]+/g, ' ') : child.textContent;
|
|
22
|
+
target.appendChild(document.createTextNode(text));
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (child.nodeType !== 1) continue;
|
|
26
|
+
const tag = child.localName.toLowerCase();
|
|
27
|
+
if (['script', 'style', 'iframe', 'object', 'embed'].includes(tag))
|
|
28
|
+
continue;
|
|
29
|
+
if (!tags.has(tag)) {
|
|
30
|
+
copy(child, target, collapse, pre);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const node = document.createElement(tag === 'font' ? 'span' : tag);
|
|
34
|
+
if (blocks.has(tag) || ['blockquote', 'ul', 'ol'].includes(tag))
|
|
35
|
+
node.style.margin = '0';
|
|
36
|
+
if (tag === 'a') {
|
|
37
|
+
const href = child.getAttribute('href');
|
|
38
|
+
if (href && !/^\s*(?:javascript|vbscript|data):/i.test(href)) {
|
|
39
|
+
node.setAttribute('href', href);
|
|
40
|
+
node.style.pointerEvents = 'auto';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (tag === 'font') {
|
|
44
|
+
if (child.hasAttribute('color'))
|
|
45
|
+
node.style.color = child.getAttribute('color');
|
|
46
|
+
if (child.hasAttribute('face'))
|
|
47
|
+
node.style.fontFamily = child.getAttribute('face');
|
|
48
|
+
}
|
|
49
|
+
for (const key of [
|
|
50
|
+
'color',
|
|
51
|
+
'background-color',
|
|
52
|
+
'font-family',
|
|
53
|
+
'font-size',
|
|
54
|
+
'font-weight',
|
|
55
|
+
'font-style',
|
|
56
|
+
'text-decoration',
|
|
57
|
+
'vertical-align',
|
|
58
|
+
'white-space',
|
|
59
|
+
]) {
|
|
60
|
+
const value = child.style?.getPropertyValue(key);
|
|
61
|
+
if (value && !/url\s*\(|expression\s*\(/i.test(value))
|
|
62
|
+
node.style.setProperty(key, value);
|
|
63
|
+
}
|
|
64
|
+
copy(child, node, collapse, pre || tag === 'pre');
|
|
65
|
+
target.appendChild(node);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const plain = (node) => {
|
|
69
|
+
let out = '';
|
|
70
|
+
for (const child of node.childNodes) {
|
|
71
|
+
if (child.nodeType === 3) out += child.textContent;
|
|
72
|
+
else if (child.nodeType === 1) {
|
|
73
|
+
if (child.localName === 'br') out += '\n';
|
|
74
|
+
else {
|
|
75
|
+
out += plain(child);
|
|
76
|
+
if (blocks.has(child.localName) && !out.endsWith('\n')) out += '\n';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
};
|
|
82
|
+
return (value) => {
|
|
83
|
+
const source = String(value.text),
|
|
84
|
+
format = value.textFormat ?? 0;
|
|
85
|
+
let entry = cache.get(value);
|
|
86
|
+
if (entry?.source === source && entry.format === format) return entry;
|
|
87
|
+
const container = entry?.container ?? document.createElement('span');
|
|
88
|
+
container.style.display = 'contents';
|
|
89
|
+
container.replaceChildren();
|
|
90
|
+
const isPlain =
|
|
91
|
+
format === 0 ||
|
|
92
|
+
(format === 2 && !/<[a-z][\w:-]*(?:\s|\/?>)/i.test(source));
|
|
93
|
+
if (isPlain) container.textContent = source;
|
|
94
|
+
else if ([1, 2, 3, 4].includes(format)) {
|
|
95
|
+
const html = format === 4 ? markdown(source) : source;
|
|
96
|
+
const template = document.createElement('template');
|
|
97
|
+
template.innerHTML = html;
|
|
98
|
+
copy(template.content, container, format === 4);
|
|
99
|
+
} else throw Error('Unsupported QML textFormat: ' + format);
|
|
100
|
+
entry = {
|
|
101
|
+
source,
|
|
102
|
+
format,
|
|
103
|
+
container,
|
|
104
|
+
plain: isPlain ? source : plain(container).replace(/\n$/, ''),
|
|
105
|
+
};
|
|
106
|
+
cache.set(value, entry);
|
|
107
|
+
return entry;
|
|
108
|
+
};
|
|
109
|
+
}
|