buninu-browser 0.4.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/README.md +22 -1
- package/frontends/wayland/README.md +50 -21
- package/frontends/wayland/index.js +14 -0
- package/frontends/wayland/input.js +24 -5
- package/frontends/wayland/urlbar.js +9 -5
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- package/lib/headless-shell.js +7 -4
- package/lib/paint/display-list.js +2 -1
- package/lib/qml/README.md +456 -0
- package/lib/qml/builtins.js +76 -5
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +47 -24
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/host.js +28 -0
- package/lib/qml/loader.js +7 -3
- package/lib/qml/models.js +1 -1
- package/lib/qml/reactive.js +26 -3
- package/lib/qml/runtime.js +263 -41
- package/lib/qml/shader/fragment.js +83 -0
- package/lib/qml/shader/qsb.js +27 -0
- package/lib/qml/shader/renderer.js +39 -0
- package/lib/qml/shader/runtime.js +61 -0
- package/lib/qml/shapes.js +9 -1
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/textures.js +4 -1
- package/lib/qml/window-constraints.js +16 -0
- package/lib/renderer/bindings.js +5 -1
- package/lib/renderer/canvas-2d.js +16 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +14 -11
- package/lib/renderer/main.js +2 -0
- package/lib/renderer/page-realm.js +4 -9
- package/lib/renderer/page-renderer.js +273 -35
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/style/computed-style.js +8 -2
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/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/omastorm-helpers/Dockerfile +35 -0
- package/test/fixtures/omastorm-helpers/check.mjs +56 -0
- package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
- package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
- package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
- package/test/fixtures/pointer-paint-batching.js +23 -0
- package/test/fixtures/qml-fonts/main.qml +10 -0
- package/test/fixtures/qml-locale/check.mjs +6 -0
- package/test/fixtures/qml-locale/main.qml +8 -0
- package/test/fixtures/qml-shader/grab.qml +14 -0
- package/test/fixtures/qml-shader/gradient.frag +5 -0
- package/test/fixtures/qml-shader/main.qml +14 -0
- package/test/fixtures/qml-shader/texture-grab.qml +25 -0
- package/test/fixtures/qml-shader/texture-size.frag +5 -0
- package/test/fixtures/qml-shader-check.mjs +24 -0
- package/test/fixtures/title-no-paint.js +20 -0
- package/test/manual-double-click.html +21 -0
- package/test/manual-mouse-buttons.html +18 -0
- package/test/manual-omastorm-engine.mjs +19 -0
- package/test/manual-omastorm-shaders.mjs +28 -0
- package/test/manual-omastorm-software.mjs +25 -0
- package/test/manual-pointer-scroll.html +43 -0
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas.test.js +21 -0
- package/test/qml-connections-timing.test.js +22 -0
- package/test/qml-controls-window.test.js +182 -0
- package/test/qml-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -0
- package/test/qml-locale.test.js +45 -0
- package/test/qml-nonvisual-visible.test.js +19 -0
- package/test/qml-shader-runtime.test.js +6 -0
- package/test/qml-shortcuts.test.js +39 -0
- package/test/qml-socket.test.js +60 -0
- package/test/qml-software-shader.test.js +55 -0
- package/test/qml-window-update.test.js +36 -0
- package/test/render-interval.test.js +33 -0
- package/test/title-no-paint.test.js +6 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +23 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// QML lifecycle adapter; all pixels and drawing commands use HTML Canvas 2D.
|
|
2
|
+
export function createQmlCanvas(api) {
|
|
3
|
+
const {effect, untrack, emit, cleanup} = api;
|
|
4
|
+
function make(v, type, element) {
|
|
5
|
+
if (type !== 'Canvas') return;
|
|
6
|
+
const canvas = document.createElement('canvas');
|
|
7
|
+
canvas.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none';
|
|
8
|
+
element.appendChild(canvas);
|
|
9
|
+
let frame, disposed = false, ready = false;
|
|
10
|
+
const callbacks = new Set();
|
|
11
|
+
Object.assign(v, {available:false, contextType:'2d', canvasSize:{width:0,height:0}});
|
|
12
|
+
const images=new Map();
|
|
13
|
+
v.loadImage=url=>{
|
|
14
|
+
url=String(url);if(images.has(url))return;
|
|
15
|
+
const image=document.createElement('img'),entry={image,status:'loading'};
|
|
16
|
+
images.set(url,entry);
|
|
17
|
+
entry.loaded=()=>{if(disposed||images.get(url)!==entry||!image.naturalWidth||!image.naturalHeight)return;entry.status='loaded';emit(v,'onImageLoaded');};
|
|
18
|
+
image.addEventListener('load',entry.loaded);
|
|
19
|
+
entry.failed=()=>{if(disposed||images.get(url)!==entry)return;entry.status='error';};
|
|
20
|
+
image.addEventListener('error',entry.failed);
|
|
21
|
+
image.style.display='none';element.appendChild(image);image.src=url;
|
|
22
|
+
};
|
|
23
|
+
v.unloadImage=url=>{const entry=images.get(String(url));if(entry){entry.image.removeEventListener('load',entry.loaded);entry.image.removeEventListener('error',entry.failed);entry.image.remove();images.delete(String(url));}};
|
|
24
|
+
v.isImageLoaded=url=>images.get(String(url))?.status==='loaded';
|
|
25
|
+
v.isImageLoading=url=>images.get(String(url))?.status==='loading';
|
|
26
|
+
v.isImageError=url=>images.get(String(url))?.status==='error';
|
|
27
|
+
const context = canvas.getContext('2d');
|
|
28
|
+
// QML Context2D.reset resets state and pixels. Reuse HTML canvas's
|
|
29
|
+
// dimension-reset behavior when the underlying 2D implementation lacks it.
|
|
30
|
+
const qmlContext = new Proxy(context, {
|
|
31
|
+
get(target, key) {
|
|
32
|
+
if (key === 'reset' && typeof target.reset !== 'function') return () => {canvas.width = canvas.width;};
|
|
33
|
+
if(key==='createImageData')return (...args)=>{
|
|
34
|
+
if(args.length!==1||typeof args[0]!=='string')return target.createImageData(...args);
|
|
35
|
+
const entry=images.get(args[0]);if(entry?.status!=='loaded')return null;
|
|
36
|
+
const tmp=document.createElement('canvas');tmp.width=entry.image.naturalWidth;tmp.height=entry.image.naturalHeight;
|
|
37
|
+
try{const ctx=tmp.getContext('2d');ctx.drawImage(entry.image,0,0);return ctx.getImageData(0,0,tmp.width,tmp.height);}
|
|
38
|
+
finally{tmp.width=tmp.height=1;}
|
|
39
|
+
};
|
|
40
|
+
const value = target[key];
|
|
41
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
42
|
+
},
|
|
43
|
+
set(target, key, value) {target[key]=(key==='fillStyle'||key==='strokeStyle') && value && typeof value==='object' && 'r' in value ? api.cssColor(value) : value;return true;},
|
|
44
|
+
});
|
|
45
|
+
v.getContext = type => type === '2d' ? qmlContext : null;
|
|
46
|
+
Object.defineProperty(v, 'context', {get:()=>v.contextType === '2d' && v.available ? v.getContext('2d') : null});
|
|
47
|
+
v.requestPaint = () => {
|
|
48
|
+
if (disposed || frame !== undefined) return;
|
|
49
|
+
frame = requestAnimationFrame(() => {
|
|
50
|
+
frame = undefined;
|
|
51
|
+
if (disposed || !ready) return;
|
|
52
|
+
untrack(() => {
|
|
53
|
+
emit(v, 'onPaint', {x:0,y:0,width:canvas.width,height:canvas.height});
|
|
54
|
+
emit(v, 'onPainted');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
v.markDirty = () => v.requestPaint();
|
|
59
|
+
v.requestAnimationFrame = callback => {
|
|
60
|
+
if (disposed) return 0;
|
|
61
|
+
const id = requestAnimationFrame(time => {callbacks.delete(id);if (!disposed) callback(time);});
|
|
62
|
+
callbacks.add(id);
|
|
63
|
+
return id;
|
|
64
|
+
};
|
|
65
|
+
v.cancelRequestAnimationFrame = id => {callbacks.delete(id);cancelAnimationFrame(id);};
|
|
66
|
+
effect(() => {
|
|
67
|
+
const w = Math.max(0,Math.ceil(v.canvasSize.width || v.width));
|
|
68
|
+
const h = Math.max(0,Math.ceil(v.canvasSize.height || v.height));
|
|
69
|
+
if (canvas.width !== w) canvas.width = w;
|
|
70
|
+
if (canvas.height !== h) canvas.height = h;
|
|
71
|
+
untrack(() => {ready=true;v.available=true;v.requestPaint();});
|
|
72
|
+
});
|
|
73
|
+
cleanup(() => {disposed=true;for(const url of images.keys())v.unloadImage(url);cancelAnimationFrame(frame);for (const id of callbacks) cancelAnimationFrame(id);callbacks.clear();});
|
|
74
|
+
}
|
|
75
|
+
return {make};
|
|
76
|
+
}
|
package/lib/qml/document.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {createQmlShaderEffects} from './shader/runtime.js';
|
|
2
|
+
import { createQmlShortcuts } from './shortcuts.js';
|
|
3
|
+
import { createQmlCanvas } from './canvas.js';
|
|
1
4
|
import { nonvisualTypes, typeProperties, createQt } from './builtins.js';
|
|
2
5
|
import { createInertWindow } from '../happy-dom/parser.js';
|
|
3
6
|
import { parseQml } from './parser.js';
|
|
@@ -25,6 +28,8 @@ const defaults = [
|
|
|
25
28
|
'Layout',
|
|
26
29
|
'Keys',
|
|
27
30
|
'opacity',
|
|
31
|
+
'smooth',
|
|
32
|
+
'antialiasing',
|
|
28
33
|
'visible',
|
|
29
34
|
'enabled',
|
|
30
35
|
'clip',
|
|
@@ -79,6 +84,7 @@ const defaults = [
|
|
|
79
84
|
'elide',
|
|
80
85
|
'lineHeight',
|
|
81
86
|
'lineHeightMode',
|
|
87
|
+
'grabToImage',
|
|
82
88
|
'forceActiveFocus',
|
|
83
89
|
'contains',
|
|
84
90
|
'mapToItem',
|
|
@@ -444,7 +450,7 @@ export function createQmlDocument(
|
|
|
444
450
|
});
|
|
445
451
|
let template = null;
|
|
446
452
|
const constants =
|
|
447
|
-
'const MediaPlayer={NoError:0,ResourceError:1,NetworkError:2,FormatError:3,AccessDeniedError:4,StoppedState:0,PlayingState:1,PausedState:2,NoMedia:0,LoadingMedia:1,LoadedMedia:2,StalledMedia:3,BufferingMedia:4,BufferedMedia:5,EndOfMedia:6,InvalidMedia:7,Infinite:-1};const VideoOutput={Stretch:0,PreserveAspectFit:1,PreserveAspectCrop:2};const XmlListModel={Null:0,Ready:1,Loading:2,Error:3};const LoggingCategory={Debug:0,Info:4,Warning:1,Critical:2,Fatal:3};const Flow={LeftToRight:0,TopToBottom:1};const ListView={Beginning:0,Center:1,End:2,Visible:3,Contain:4,SnapPosition:5,Vertical:2,Horizontal:1,NoHighlightRange:0};const GridView={...ListView,FlowLeftToRight:0,FlowTopToBottom:1};const Binding={RestoreNone:0,RestoreBinding:1,RestoreValue:2,RestoreBindingOrValue:3};const Gradient={Vertical:0,Horizontal:1};const PointerHandler={CanTakeOverFromAnything:31,ApprovesTakeOverByAnything:511,TakeOverForbidden:0};const Drag={None:0,Automatic:1,Internal:2};const Application={styleHints:{wheelScrollLines:3,mouseDoubleClickInterval:400}};const Flickable={AutoFlickDirection:0,HorizontalFlick:1,VerticalFlick:2,HorizontalAndVerticalFlick:3,StopAtBounds:0,DragOverBounds:1,OvershootBounds:2,DragAndOvershootBounds:3};const Image={Null:0,Ready:1,Loading:2,Error:3,Stretch:0,PreserveAspectFit:1,PreserveAspectCrop:2,Tile:3,Pad:6};const TextInput={AlignLeft:1,AlignRight:2,AlignHCenter:4,AlignTop:32,AlignBottom:64,AlignVCenter:128,Normal:0,Password:2,NoEcho:1,PasswordEchoOnEdit:3,NativeRendering:1};const TextEdit=TextInput;const Text={AlignLeft:1,AlignRight:2,AlignHCenter:4,AlignTop:32,AlignBottom:64,AlignVCenter:128,PlainText:0,RichText:1,AutoText:2,StyledText:3,MarkdownText:4,NoWrap:0,WordWrap:1,WrapAnywhere:2,Wrap:3,WrapAtWordBoundaryOrAnywhere:3,ElideNone:0,ElideLeft:1,ElideRight:2,ElideMiddle:3,ProportionalHeight:0,FixedHeight:1};const Font={MixedCase:0,AllUppercase:1,AllLowercase:2,SmallCaps:3,Capitalize:4};const Accessible={Button:43,CheckBox:44};const Shape={CurveRenderer:1};const ShapePath={SquareCap:16,FlatCap:0,RoundCap:32,MiterJoin:0,BevelJoin:64,RoundJoin:128,DashLine:2,WindingFill:1,OddEvenFill:0};const Easing={Linear:0,InQuad:1,OutQuad:2,InOutQuad:3,InCubic:5,OutCubic:6,InOutCubic:7,OutQuart:10,OutQuint:14,OutSine:18,OutExpo:22,OutBack:34};const Animation={Infinite:-1};const Component={Null:0,Ready:1,Loading:2,Error:3};const Loader=Component;const TapHandler={ReleaseWithinBounds:1};const Qt=r.Qt,Quickshell=r.Quickshell,Hyprland=r.Hyprland,FileViewError={FileNotFound:"ENOENT"};';
|
|
453
|
+
'const Locale={LongFormat:0,ShortFormat:1,NarrowFormat:2,MetricSystem:0,ImperialUSSystem:1,ImperialUKSystem:2};const StandardKey={Copy:9,Paste:10,Cut:8,SelectAll:26,Undo:11,Redo:12};const ShaderEffect={Uncompiled:0,Compiled:1,Error:2,NoCulling:0,BackFaceCulling:1,FrontFaceCulling:2};const MediaPlayer={NoError:0,ResourceError:1,NetworkError:2,FormatError:3,AccessDeniedError:4,StoppedState:0,PlayingState:1,PausedState:2,NoMedia:0,LoadingMedia:1,LoadedMedia:2,StalledMedia:3,BufferingMedia:4,BufferedMedia:5,EndOfMedia:6,InvalidMedia:7,Infinite:-1};const VideoOutput={Stretch:0,PreserveAspectFit:1,PreserveAspectCrop:2};const XmlListModel={Null:0,Ready:1,Loading:2,Error:3};const LoggingCategory={Debug:0,Info:4,Warning:1,Critical:2,Fatal:3};const Flow={LeftToRight:0,TopToBottom:1};const ListView={Beginning:0,Center:1,End:2,Visible:3,Contain:4,SnapPosition:5,Vertical:2,Horizontal:1,NoHighlightRange:0};const GridView={...ListView,FlowLeftToRight:0,FlowTopToBottom:1};const Binding={RestoreNone:0,RestoreBinding:1,RestoreValue:2,RestoreBindingOrValue:3};const Gradient={Vertical:0,Horizontal:1};const PointerHandler={CanTakeOverFromAnything:31,ApprovesTakeOverByAnything:511,TakeOverForbidden:0};const Drag={None:0,Automatic:1,Internal:2};const Application={styleHints:{wheelScrollLines:3,mouseDoubleClickInterval:400}};const Flickable={AutoFlickDirection:0,HorizontalFlick:1,VerticalFlick:2,HorizontalAndVerticalFlick:3,StopAtBounds:0,DragOverBounds:1,OvershootBounds:2,DragAndOvershootBounds:3};const Image={Null:0,Ready:1,Loading:2,Error:3,Stretch:0,PreserveAspectFit:1,PreserveAspectCrop:2,Tile:3,Pad:6};const TextInput={AlignLeft:1,AlignRight:2,AlignHCenter:4,AlignTop:32,AlignBottom:64,AlignVCenter:128,Normal:0,Password:2,NoEcho:1,PasswordEchoOnEdit:3,NativeRendering:1};const TextEdit=TextInput;const Text={AlignLeft:1,AlignRight:2,AlignHCenter:4,AlignTop:32,AlignBottom:64,AlignVCenter:128,PlainText:0,RichText:1,AutoText:2,StyledText:3,MarkdownText:4,NoWrap:0,WordWrap:1,WrapAnywhere:2,Wrap:3,WrapAtWordBoundaryOrAnywhere:3,ElideNone:0,ElideLeft:1,ElideRight:2,ElideMiddle:3,ProportionalHeight:0,FixedHeight:1};const Font={MixedCase:0,AllUppercase:1,AllLowercase:2,SmallCaps:3,Capitalize:4};const Accessible={Button:43,CheckBox:44};const Shape={CurveRenderer:1};const ShapePath={SquareCap:16,FlatCap:0,RoundCap:32,MiterJoin:0,BevelJoin:64,RoundJoin:128,DashLine:2,WindingFill:1,OddEvenFill:0};const Easing={Linear:0,InQuad:1,OutQuad:2,InOutQuad:3,InCubic:5,OutCubic:6,InOutCubic:7,OutQuart:10,OutQuint:14,OutSine:18,OutExpo:22,OutBack:34};const Animation={Infinite:-1};const Component={Null:0,Ready:1,Loading:2,Error:3};const Loader=Component;const TapHandler={ReleaseWithinBounds:1};const Qt=r.Qt,Quickshell=r.Quickshell,Hyprland=r.Hyprland,FileViewError={FileNotFound:"ENOENT"};';
|
|
448
454
|
const ref = (record) =>
|
|
449
455
|
record.external ?? `${generatedPrefix}n${record.index}`;
|
|
450
456
|
function context(
|
|
@@ -538,7 +544,7 @@ export function createQmlDocument(
|
|
|
538
544
|
'alias',
|
|
539
545
|
'double',
|
|
540
546
|
'url',
|
|
541
|
-
'point',
|
|
547
|
+
'point','vector2d','vector3d','vector4d',
|
|
542
548
|
'size',
|
|
543
549
|
'rect',
|
|
544
550
|
].includes(p.propertyType) &&
|
|
@@ -554,7 +560,7 @@ export function createQmlDocument(
|
|
|
554
560
|
} else if (
|
|
555
561
|
!record.names.has(p.key) &&
|
|
556
562
|
!groups.has(p.key) &&
|
|
557
|
-
!/^(?:Keys\.|Accessible\.|Drag\.|ListView\.|GridView\.)?on[A-Z_]/.test(
|
|
563
|
+
!/^(?:Keys\.|Accessible\.|Drag\.|Window\.|ListView\.|GridView\.)?on[A-Z_]/.test(
|
|
558
564
|
p.key,
|
|
559
565
|
) &&
|
|
560
566
|
!['Component.onCompleted', 'Component.onDestruction'].includes(p.key)
|
|
@@ -567,7 +573,7 @@ export function createQmlDocument(
|
|
|
567
573
|
// Each component layer connects its own signal handler. Overriding a
|
|
568
574
|
// property binding must not discard the base component's connections.
|
|
569
575
|
if (previous && !p.method && !p.signal &&
|
|
570
|
-
(/^(?:Keys\.|Accessible\.|Drag\.|ListView\.|GridView\.)?on[A-Z_]/.test(p.key) ||
|
|
576
|
+
(/^(?:Keys\.|Accessible\.|Drag\.|Window\.|ListView\.|GridView\.)?on[A-Z_]/.test(p.key) ||
|
|
571
577
|
['Component.onCompleted', 'Component.onDestruction'].includes(p.key)))
|
|
572
578
|
(record.inheritedHandlers ??= []).push(previous);
|
|
573
579
|
if (previous?.p.readonly && !p.custom)
|
|
@@ -622,7 +628,7 @@ export function createQmlDocument(
|
|
|
622
628
|
function buildPropertyObject(p, record, ctx, chain) {
|
|
623
629
|
const slot = aliasSlot(record, p.key);
|
|
624
630
|
const child = build(
|
|
625
|
-
p.key
|
|
631
|
+
['sourceComponent','cursorDelegate'].includes(p.key) && p.object.type !== 'Component'
|
|
626
632
|
? { type: 'Component', properties: [], children: [p.object] }
|
|
627
633
|
: p.object,
|
|
628
634
|
slot ?? record,
|
|
@@ -664,7 +670,7 @@ export function createQmlDocument(
|
|
|
664
670
|
if (
|
|
665
671
|
p.object &&
|
|
666
672
|
!(
|
|
667
|
-
['Repeater', 'ListView', 'GridView'].includes(record.type) &&
|
|
673
|
+
['Instantiator', 'Repeater', 'ListView', 'GridView'].includes(record.type) &&
|
|
668
674
|
p.key === 'delegate'
|
|
669
675
|
)
|
|
670
676
|
)
|
|
@@ -676,7 +682,7 @@ export function createQmlDocument(
|
|
|
676
682
|
const type = resolution?.builtin ?? object.type;
|
|
677
683
|
if (!types.has(type)) throw Error(`Unsupported QML type: ${object.type}`);
|
|
678
684
|
if (records.length >= 2000) throw Error('QML expanded object limit');
|
|
679
|
-
const svgTag = { ShapePath: 'g', PathSvg: 'path', PathRectangle: 'rect' }[
|
|
685
|
+
const svgTag = { ShapePath: 'g', PathMultiline: 'path', PathSvg: 'path', PathRectangle: 'rect' }[
|
|
680
686
|
type
|
|
681
687
|
];
|
|
682
688
|
const element = svgTag
|
|
@@ -727,12 +733,12 @@ export function createQmlDocument(
|
|
|
727
733
|
if (type === 'Button') element.setAttribute('type', 'button');
|
|
728
734
|
(parent?.element ?? document.body).appendChild(element);
|
|
729
735
|
addProperties(record, object, ctx);
|
|
730
|
-
if (['Repeater', 'ListView', 'GridView', 'Component'].includes(type)) {
|
|
736
|
+
if (['Instantiator', 'Repeater', 'ListView', 'GridView', 'Component'].includes(type)) {
|
|
731
737
|
const delegate =
|
|
732
738
|
type === 'Component'
|
|
733
739
|
? object.children[0]
|
|
734
740
|
: (record.properties.get('delegate')?.p.object ??
|
|
735
|
-
(
|
|
741
|
+
(['Repeater', 'Instantiator'].includes(type) ? object.children[0] : null));
|
|
736
742
|
if (!delegate) throw Error(type + ' requires an object delegate');
|
|
737
743
|
if (type === 'Component' && object.children.length !== 1)
|
|
738
744
|
throw Error('Component requires exactly one root');
|
|
@@ -745,9 +751,11 @@ export function createQmlDocument(
|
|
|
745
751
|
kind:
|
|
746
752
|
type === 'Component'
|
|
747
753
|
? 'component'
|
|
748
|
-
: type === '
|
|
749
|
-
? '
|
|
750
|
-
: '
|
|
754
|
+
: type === 'Instantiator'
|
|
755
|
+
? 'instantiator'
|
|
756
|
+
: type === 'Repeater'
|
|
757
|
+
? 'repeater'
|
|
758
|
+
: 'view',
|
|
751
759
|
};
|
|
752
760
|
template = group;
|
|
753
761
|
const delegateContext = {
|
|
@@ -761,7 +769,7 @@ export function createQmlDocument(
|
|
|
761
769
|
if (p.key === 'id') delegateContext.ids.delete(p.code);
|
|
762
770
|
group.root = build(
|
|
763
771
|
delegate,
|
|
764
|
-
['ListView', 'GridView'].includes(type) ? record : parent,
|
|
772
|
+
['Instantiator', 'ListView', 'GridView'].includes(type) ? record : parent,
|
|
765
773
|
delegateContext,
|
|
766
774
|
chain,
|
|
767
775
|
);
|
|
@@ -775,7 +783,7 @@ export function createQmlDocument(
|
|
|
775
783
|
if (
|
|
776
784
|
p.object &&
|
|
777
785
|
!(
|
|
778
|
-
['Repeater', 'ListView', 'GridView'].includes(record.type) &&
|
|
786
|
+
['Instantiator', 'Repeater', 'ListView', 'GridView'].includes(record.type) &&
|
|
779
787
|
p.key === 'delegate'
|
|
780
788
|
)
|
|
781
789
|
)
|
|
@@ -783,7 +791,7 @@ export function createQmlDocument(
|
|
|
783
791
|
if (
|
|
784
792
|
type !== 'Component' &&
|
|
785
793
|
!(
|
|
786
|
-
|
|
794
|
+
['Repeater', 'Instantiator'].includes(type) &&
|
|
787
795
|
!object.properties.some((p) => p.key === 'delegate')
|
|
788
796
|
)
|
|
789
797
|
)
|
|
@@ -804,8 +812,7 @@ export function createQmlDocument(
|
|
|
804
812
|
record.template.root === record
|
|
805
813
|
) &&
|
|
806
814
|
!(
|
|
807
|
-
record.template?.root === record &&
|
|
808
|
-
['index', 'modelData'].includes(p.key)
|
|
815
|
+
record.template?.root === record && record.template.kind !== 'component'
|
|
809
816
|
)
|
|
810
817
|
)
|
|
811
818
|
throw Error(`Missing required QML property: ${p.key}`);
|
|
@@ -813,7 +820,7 @@ export function createQmlDocument(
|
|
|
813
820
|
// Do not parse a second copy of the entire runtime for every lazy pane.
|
|
814
821
|
const runtimeSource = component
|
|
815
822
|
? 'globalThis.__qmlRuntime'
|
|
816
|
-
: `globalThis.__qmlRuntime??=(${createQmlRuntime.toString()})((${createReactive.toString()})(),(${createQt.toString()})(),(${createQmlModels.toString()}),(${createQmlText.toString()}),(${createQmlShapes.toString()}),(${createQmlImages.toString()}),(${createQmlTextures.toString()}),(${createQmlMedia.toString()}))`;
|
|
823
|
+
: `globalThis.__qmlRuntime??=(${createQmlRuntime.toString()})((${createReactive.toString()})(),(${createQt.toString()})(),(${createQmlModels.toString()}),(${createQmlText.toString()}),(${createQmlShapes.toString()}),(${createQmlImages.toString()}),(${createQmlTextures.toString()}),(${createQmlMedia.toString()}),(${createQmlCanvas.toString()}),(${createQmlShortcuts.toString()}),(${createQmlShaderEffects.toString()}))`;
|
|
817
824
|
const code = [
|
|
818
825
|
`(()=>{const r=${runtimeSource};if(!r)throw Error('QML component requires an initialized page');const ${runtimeRef}=r;`,
|
|
819
826
|
constants,
|
|
@@ -887,6 +894,8 @@ export function createQmlDocument(
|
|
|
887
894
|
if (
|
|
888
895
|
group &&
|
|
889
896
|
![
|
|
897
|
+
'Canvas',
|
|
898
|
+
'ShaderEffect',
|
|
890
899
|
'ShaderEffectSource',
|
|
891
900
|
'LinearGradient',
|
|
892
901
|
'XmlListModel',
|
|
@@ -894,7 +903,7 @@ export function createQmlDocument(
|
|
|
894
903
|
'WorkerScript',
|
|
895
904
|
'LoggingCategory',
|
|
896
905
|
'Flow',
|
|
897
|
-
'RowLayout',
|
|
906
|
+
'GridLayout', 'RowLayout',
|
|
898
907
|
'ColumnLayout',
|
|
899
908
|
'ListView',
|
|
900
909
|
'GridView',
|
|
@@ -905,7 +914,7 @@ export function createQmlDocument(
|
|
|
905
914
|
'Binding',
|
|
906
915
|
'Gradient',
|
|
907
916
|
'GradientStop',
|
|
908
|
-
'PathRectangle',
|
|
917
|
+
'PathMultiline', 'PathRectangle',
|
|
909
918
|
'IpcHandler',
|
|
910
919
|
'DropArea',
|
|
911
920
|
'DragHandler',
|
|
@@ -924,6 +933,7 @@ export function createQmlDocument(
|
|
|
924
933
|
'PropertyAction',
|
|
925
934
|
'Timer',
|
|
926
935
|
'Connections',
|
|
936
|
+
'Socket',
|
|
927
937
|
'Process',
|
|
928
938
|
'StdioCollector',
|
|
929
939
|
'SplitParser',
|
|
@@ -949,7 +959,7 @@ export function createQmlDocument(
|
|
|
949
959
|
'FocusScope',
|
|
950
960
|
'Loader',
|
|
951
961
|
'Component',
|
|
952
|
-
'Repeater',
|
|
962
|
+
'Shortcut', 'Instantiator', 'Repeater',
|
|
953
963
|
'TapHandler',
|
|
954
964
|
'HoverHandler',
|
|
955
965
|
].includes(record.type)
|
|
@@ -988,6 +998,12 @@ export function createQmlDocument(
|
|
|
988
998
|
? '""'
|
|
989
999
|
: p.propertyType === 'color'
|
|
990
1000
|
? '"transparent"'
|
|
1001
|
+
: p.propertyType === 'size' ? '({width:0,height:0})'
|
|
1002
|
+
: p.propertyType === 'point' ? '({x:0,y:0})'
|
|
1003
|
+
: p.propertyType === 'rect' ? '({x:0,y:0,width:0,height:0})'
|
|
1004
|
+
: p.propertyType === 'vector2d' ? '({x:0,y:0})'
|
|
1005
|
+
: p.propertyType === 'vector3d' ? '({x:0,y:0,z:0})'
|
|
1006
|
+
: p.propertyType === 'vector4d' ? '({x:0,y:0,z:0,w:0})'
|
|
991
1007
|
: p.propertyType === 'var'
|
|
992
1008
|
? 'undefined'
|
|
993
1009
|
: 'null';
|
|
@@ -1015,6 +1031,12 @@ export function createQmlDocument(
|
|
|
1015
1031
|
code.push(
|
|
1016
1032
|
`${ref(group.root)}.index=index;${ref(group.root)}.modelData=modelData;r.attachDelegate(${ref(group.root)},index,${ref(group.owner)});`,
|
|
1017
1033
|
);
|
|
1034
|
+
if(group && group.kind!=='component'){
|
|
1035
|
+
for(const {p} of group.root.properties.values())if(p.required&&!['index','modelData'].includes(p.key)){
|
|
1036
|
+
code.push(`if(modelData==null||!Object.hasOwn(Object(modelData),${JSON.stringify(p.key)}))throw Error('Missing model role: '+${JSON.stringify(p.key)});`);
|
|
1037
|
+
code.push(`r.bind(${ref(group.root)},${JSON.stringify(p.key)},()=>modelData[${JSON.stringify(p.key)}],false,${JSON.stringify(p.propertyType)});`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1018
1040
|
for (const record of members)
|
|
1019
1041
|
if (record.delegate) {
|
|
1020
1042
|
code.push(
|
|
@@ -1036,7 +1058,7 @@ export function createQmlDocument(
|
|
|
1036
1058
|
const expr = expression(p, scope);
|
|
1037
1059
|
if (
|
|
1038
1060
|
record.type === 'Connections' &&
|
|
1039
|
-
/^(?:Keys\.|Accessible\.|Drag\.|ListView\.|GridView\.)?on[A-Z_]/.test(
|
|
1061
|
+
/^(?:Keys\.|Accessible\.|Drag\.|Window\.|ListView\.|GridView\.)?on[A-Z_]/.test(
|
|
1040
1062
|
p.key,
|
|
1041
1063
|
)
|
|
1042
1064
|
)
|
|
@@ -1112,7 +1134,7 @@ export function createQmlDocument(
|
|
|
1112
1134
|
continue;
|
|
1113
1135
|
}
|
|
1114
1136
|
if (
|
|
1115
|
-
/^(?:Keys\.|Accessible\.|Drag\.|ListView\.|GridView\.)?on[A-Z_]/.test(
|
|
1137
|
+
/^(?:Keys\.|Accessible\.|Drag\.|Window\.|ListView\.|GridView\.)?on[A-Z_]/.test(
|
|
1116
1138
|
p.key,
|
|
1117
1139
|
) &&
|
|
1118
1140
|
p.key !== 'onClicked'
|
|
@@ -1143,7 +1165,8 @@ export function createQmlDocument(
|
|
|
1143
1165
|
`${override ? `if(!Object.hasOwn(properties,${JSON.stringify(p.key)})) ` : ''}r.bind(${target},${JSON.stringify(key)},()=>(${value}),${!!p.readonly},${JSON.stringify(p.propertyType)});`,
|
|
1144
1166
|
);
|
|
1145
1167
|
}
|
|
1146
|
-
|
|
1168
|
+
// Nonvisual DOM nodes are already hidden. Their QML visible property
|
|
1169
|
+
// may be an application-defined readonly value; never overwrite it.
|
|
1147
1170
|
}
|
|
1148
1171
|
if ((component && !group) || group?.kind === 'component') {
|
|
1149
1172
|
const root = group?.root ?? entryContext.root;
|
package/lib/qml/fonts.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {readdir, realpath, stat} from 'node:fs/promises';
|
|
2
|
+
import {join, resolve} from 'node:path';
|
|
3
|
+
import {registerTypeface} from '../renderer/font-loading.js';
|
|
4
|
+
|
|
5
|
+
// Capture before a host-mode document can replace globals/environment.
|
|
6
|
+
const directory = process.env.QT_QPA_FONTDIR || '';
|
|
7
|
+
const cache = new WeakMap();
|
|
8
|
+
const MAX_FILE = 32 * 1024 * 1024;
|
|
9
|
+
const MAX_TOTAL = 128 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
// QT_QPA_FONTDIR is one directory, not a PATH-style list. Recursion is our
|
|
12
|
+
// extension; canonical paths prevent directory symlink cycles and duplicates.
|
|
13
|
+
export async function qmlFontFiles(root, warn = () => {}) {
|
|
14
|
+
const files = [], seen = new Set();
|
|
15
|
+
async function walk(path, depth = 0) {
|
|
16
|
+
if (depth > 16 || seen.size >= 4096) return;
|
|
17
|
+
try {
|
|
18
|
+
path = await realpath(path);
|
|
19
|
+
if (seen.has(path)) return;
|
|
20
|
+
seen.add(path);
|
|
21
|
+
const info = await stat(path);
|
|
22
|
+
if (info.isDirectory()) {
|
|
23
|
+
for (const name of (await readdir(path)).sort()) await walk(join(path, name), depth + 1);
|
|
24
|
+
} else if (info.isFile() && /\.(ttf|otf)$/i.test(path)) files.push({path, size:info.size});
|
|
25
|
+
} catch (error) { warn(`QML font: ${path}: ${error.message}`); }
|
|
26
|
+
}
|
|
27
|
+
if (root) await walk(resolve(root));
|
|
28
|
+
return files.sort((a,b)=>Number(!/regular/i.test(a.path))-Number(!/regular/i.test(b.path)) || a.path.localeCompare(b.path));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// One private collection per graphics instance, shared by its local QML pages.
|
|
32
|
+
// The ordinary HTML collection and font aliases are never mutated.
|
|
33
|
+
export function loadQmlFonts(base, warn = () => {}, root = directory) {
|
|
34
|
+
if (!root) return Promise.resolve(base);
|
|
35
|
+
let roots = cache.get(base);
|
|
36
|
+
if (!roots) cache.set(base, roots = new Map());
|
|
37
|
+
const key = resolve(root);
|
|
38
|
+
if (!roots.has(key)) roots.set(key, build(base, key, warn));
|
|
39
|
+
return roots.get(key);
|
|
40
|
+
}
|
|
41
|
+
async function build(base, root, warn) {
|
|
42
|
+
const {CanvasKit} = base;
|
|
43
|
+
const extra = [];
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (const file of await qmlFontFiles(root, warn)) {
|
|
46
|
+
if (file.size > MAX_FILE || total + file.size > MAX_TOTAL || extra.length >= 128) {
|
|
47
|
+
warn(`QML font: skipped ${file.path}: font loading limit`);continue;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const bytes = await Bun.file(file.path).bytes();
|
|
51
|
+
if (bytes.length > MAX_FILE || total + bytes.length > MAX_TOTAL) continue;
|
|
52
|
+
const typeface = CanvasKit.Typeface.MakeTypefaceFromData(bytes);
|
|
53
|
+
if (!typeface) throw Error('unsupported or invalid font');
|
|
54
|
+
total += bytes.length;
|
|
55
|
+
extra.push({family:`Buninu QML Fallback ${extra.length}`, name:typeface.getFamilyName(), typeface});
|
|
56
|
+
} catch(error) {warn(`QML font: ${file.path}: ${error.message}`);}
|
|
57
|
+
}
|
|
58
|
+
if (!extra.length) return base;
|
|
59
|
+
const provider = CanvasKit.TypefaceFontProvider.Make();
|
|
60
|
+
const builtinTypefaces = [...base.builtinTypefaces, ...extra];
|
|
61
|
+
for (const face of builtinTypefaces) registerTypeface(CanvasKit, provider, face.typeface, face.family);
|
|
62
|
+
const fontCollection = CanvasKit.FontCollection.Make();
|
|
63
|
+
fontCollection.setDefaultFontManager(provider);
|
|
64
|
+
fontCollection.enableFontFallback();
|
|
65
|
+
const loadedFamilies = new Map(base.loadedFamilies);
|
|
66
|
+
for (const face of extra) if (!loadedFamilies.has(face.name.toLowerCase())) loadedFamilies.set(face.name.toLowerCase(), face.family);
|
|
67
|
+
return {...base, fontCollection, loadedFamilies, builtinTypefaces,
|
|
68
|
+
fontFamilies:[...base.fontFamilies, ...extra.map(face=>face.family)]};
|
|
69
|
+
}
|
package/lib/qml/host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {readClipboard, writeClipboard} from '../clipboard.js';
|
|
2
|
+
import { createConnection } from 'node:net';
|
|
2
3
|
import { Worker } from 'node:worker_threads';
|
|
3
4
|
import { loadQmlGraphSync } from './loader.js';
|
|
4
5
|
import { createQmlDocument } from './document.js';
|
|
@@ -233,6 +234,28 @@ export function createQmlHost(url) {
|
|
|
233
234
|
resources.add(stop);
|
|
234
235
|
return stop;
|
|
235
236
|
},
|
|
237
|
+
socket(name, handlers) {
|
|
238
|
+
check();
|
|
239
|
+
const socket = createConnection({path: path(name)});
|
|
240
|
+
let stopped = false;
|
|
241
|
+
socket.setEncoding('utf8');
|
|
242
|
+
socket.on('connect', () => { if (!stopped) handlers.open(); });
|
|
243
|
+
socket.on('data', chunk => { if (!stopped) handlers.data(chunk); });
|
|
244
|
+
socket.on('error', error => { if (!stopped) handlers.error(error.code || String(error)); });
|
|
245
|
+
socket.on('close', () => {
|
|
246
|
+
resources.delete(stop);
|
|
247
|
+
if (!stopped) { stopped = true; handlers.close(); }
|
|
248
|
+
});
|
|
249
|
+
const stop = () => {
|
|
250
|
+
stopped = true;
|
|
251
|
+
resources.delete(stop);
|
|
252
|
+
socket.destroy();
|
|
253
|
+
};
|
|
254
|
+
stop.write = text => { if (!stopped && !socket.connecting) socket.write(String(text)); };
|
|
255
|
+
stop.flush = () => {}; // node:net queues writes immediately, including backpressure.
|
|
256
|
+
resources.add(stop);
|
|
257
|
+
return stop;
|
|
258
|
+
},
|
|
236
259
|
run(command, callback, options = {}) {
|
|
237
260
|
check();
|
|
238
261
|
if (!Array.isArray(command) || !command.length)
|
|
@@ -311,6 +334,11 @@ export function createQmlHost(url) {
|
|
|
311
334
|
});
|
|
312
335
|
return stop;
|
|
313
336
|
},
|
|
337
|
+
trackResource(dispose) {
|
|
338
|
+
check();let stopped=false;
|
|
339
|
+
const stop=()=>{if(stopped)return;stopped=true;resources.delete(stop);dispose();};
|
|
340
|
+
resources.add(stop);return stop;
|
|
341
|
+
},
|
|
314
342
|
dispose() {
|
|
315
343
|
if (closed) return;
|
|
316
344
|
closed = true;
|
package/lib/qml/loader.js
CHANGED
|
@@ -4,6 +4,8 @@ import { parseQml } from './parser.js';
|
|
|
4
4
|
import { parse } from '../../vendor/acorn/dist/acorn.mjs';
|
|
5
5
|
export const builtinTypes = new Set([
|
|
6
6
|
'MediaPlayer', 'AudioOutput', 'VideoOutput', 'IdleInhibitor',
|
|
7
|
+
'Canvas',
|
|
8
|
+
'ShaderEffect',
|
|
7
9
|
'ShaderEffectSource',
|
|
8
10
|
'LinearGradient',
|
|
9
11
|
'XmlListModel',
|
|
@@ -11,7 +13,7 @@ export const builtinTypes = new Set([
|
|
|
11
13
|
'WorkerScript',
|
|
12
14
|
'LoggingCategory',
|
|
13
15
|
'Flow',
|
|
14
|
-
'RowLayout',
|
|
16
|
+
'GridLayout', 'RowLayout',
|
|
15
17
|
'ColumnLayout',
|
|
16
18
|
'ListView',
|
|
17
19
|
'GridView',
|
|
@@ -22,7 +24,7 @@ export const builtinTypes = new Set([
|
|
|
22
24
|
'Binding',
|
|
23
25
|
'Gradient',
|
|
24
26
|
'GradientStop',
|
|
25
|
-
'PathRectangle',
|
|
27
|
+
'PathMultiline', 'PathRectangle',
|
|
26
28
|
'IpcHandler',
|
|
27
29
|
'DropArea',
|
|
28
30
|
'DragHandler',
|
|
@@ -47,10 +49,11 @@ export const builtinTypes = new Set([
|
|
|
47
49
|
'Timer',
|
|
48
50
|
'Connections',
|
|
49
51
|
'FileView',
|
|
52
|
+
'Socket',
|
|
50
53
|
'Process',
|
|
51
54
|
'StdioCollector',
|
|
52
55
|
'SplitParser',
|
|
53
|
-
'Repeater',
|
|
56
|
+
'Shortcut', 'Instantiator', 'Repeater',
|
|
54
57
|
'TapHandler',
|
|
55
58
|
'HoverHandler',
|
|
56
59
|
'Row',
|
|
@@ -80,6 +83,7 @@ const builtinModules = new Set([
|
|
|
80
83
|
'QtMultimedia',
|
|
81
84
|
'Quickshell.Wayland',
|
|
82
85
|
'QtQuick',
|
|
86
|
+
'QtQuick.Window',
|
|
83
87
|
'QtQuick.Controls',
|
|
84
88
|
'QtQuick.Shapes',
|
|
85
89
|
'QtQuick.Layouts',
|
package/lib/qml/models.js
CHANGED
|
@@ -618,5 +618,5 @@ export function createQmlModels(api) {
|
|
|
618
618
|
});
|
|
619
619
|
effect(layout);
|
|
620
620
|
}
|
|
621
|
-
return { make, start, attachDelegate };
|
|
621
|
+
return { make, start, attachDelegate, rows:model=>{const data=lists.get(model)||xmlModels.get(model);if(!data)return null;data.version;return data.rows;} };
|
|
622
622
|
}
|
package/lib/qml/reactive.js
CHANGED
|
@@ -4,17 +4,38 @@ export function createReactive() {
|
|
|
4
4
|
let active = null,
|
|
5
5
|
pending = new Set(),
|
|
6
6
|
scheduled = false;
|
|
7
|
+
let invalidationDepth = 0, drainingSync = false;
|
|
8
|
+
const synchronous = new Set();
|
|
7
9
|
const track = new WeakMap(),
|
|
8
10
|
raw = new WeakMap();
|
|
9
11
|
const mark = (job) => {
|
|
10
12
|
if (job.stopped || job.dirty || job.running) return;
|
|
11
13
|
job.dirty = true;
|
|
12
14
|
pending.add(job);
|
|
15
|
+
if (job.sync) synchronous.add(job);
|
|
13
16
|
job.invalidate?.();
|
|
14
17
|
};
|
|
15
18
|
const invalidate = (target, key) => {
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
invalidationDepth++;
|
|
20
|
+
try {
|
|
21
|
+
for (const job of track.get(raw.get(target) ?? target)?.get(key) ?? []) mark(job);
|
|
22
|
+
} finally {
|
|
23
|
+
invalidationDepth--;
|
|
24
|
+
}
|
|
25
|
+
// Invalidate the whole dependency chain before synchronous observers read
|
|
26
|
+
// it. Their handlers may write more properties; drain without recursive
|
|
27
|
+
// Set iteration, while ordinary rendering effects remain batched.
|
|
28
|
+
if (invalidationDepth || drainingSync) return;
|
|
29
|
+
drainingSync = true;
|
|
30
|
+
try {
|
|
31
|
+
let passes = 0;
|
|
32
|
+
while (synchronous.size) {
|
|
33
|
+
if (++passes > 100) throw new Error('QML binding cycle');
|
|
34
|
+
const jobs = [...synchronous];
|
|
35
|
+
synchronous.clear();
|
|
36
|
+
for (const job of jobs) { pending.delete(job); run(job); }
|
|
37
|
+
}
|
|
38
|
+
} finally { drainingSync = false; }
|
|
18
39
|
};
|
|
19
40
|
const clean = (job) => {
|
|
20
41
|
for (const set of job.deps) set.delete(job);
|
|
@@ -105,9 +126,10 @@ export function createReactive() {
|
|
|
105
126
|
raw.set(proxy, initial);
|
|
106
127
|
return proxy;
|
|
107
128
|
};
|
|
108
|
-
const effect = (fn) => {
|
|
129
|
+
const effect = (fn, { sync = false } = {}) => {
|
|
109
130
|
const job = {
|
|
110
131
|
fn,
|
|
132
|
+
sync,
|
|
111
133
|
deps: new Set(),
|
|
112
134
|
stopped: false,
|
|
113
135
|
dirty: true,
|
|
@@ -125,6 +147,7 @@ export function createReactive() {
|
|
|
125
147
|
job.stopped = true;
|
|
126
148
|
clean(job);
|
|
127
149
|
pending.delete(job);
|
|
150
|
+
synchronous.delete(job);
|
|
128
151
|
};
|
|
129
152
|
stop.run = () => run(job);
|
|
130
153
|
Object.defineProperty(stop, 'onInvalidate', {
|