@react-x11/components 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.
Files changed (314) hide show
  1. package/README.md +131 -4
  2. package/dist/charts/components.d.ts +3 -1
  3. package/dist/charts/components.d.ts.map +1 -1
  4. package/dist/charts/components.js +8 -4
  5. package/dist/charts/components.js.map +1 -1
  6. package/dist/charts/node.d.ts +13 -6
  7. package/dist/charts/node.d.ts.map +1 -1
  8. package/dist/charts/node.js +75 -24
  9. package/dist/charts/node.js.map +1 -1
  10. package/dist/charts/render.d.ts +4 -0
  11. package/dist/charts/render.d.ts.map +1 -1
  12. package/dist/charts/render.js +11 -6
  13. package/dist/charts/render.js.map +1 -1
  14. package/dist/code-editor/node.d.ts +47 -16
  15. package/dist/code-editor/node.d.ts.map +1 -1
  16. package/dist/code-editor/node.js +121 -51
  17. package/dist/code-editor/node.js.map +1 -1
  18. package/dist/flow/draw.d.ts +19 -1
  19. package/dist/flow/draw.d.ts.map +1 -1
  20. package/dist/flow/draw.js +64 -35
  21. package/dist/flow/draw.js.map +1 -1
  22. package/dist/flow/index.d.ts.map +1 -1
  23. package/dist/flow/index.js +24 -8
  24. package/dist/flow/index.js.map +1 -1
  25. package/dist/flow/model.d.ts +4 -2
  26. package/dist/flow/model.d.ts.map +1 -1
  27. package/dist/flow/model.js +4 -2
  28. package/dist/flow/model.js.map +1 -1
  29. package/dist/flow/node.d.ts +40 -9
  30. package/dist/flow/node.d.ts.map +1 -1
  31. package/dist/flow/node.js +142 -53
  32. package/dist/flow/node.js.map +1 -1
  33. package/dist/flow/types.d.ts +46 -21
  34. package/dist/flow/types.d.ts.map +1 -1
  35. package/dist/formula/index.d.ts +4 -2
  36. package/dist/formula/index.d.ts.map +1 -1
  37. package/dist/formula/index.js.map +1 -1
  38. package/dist/formula/node.d.ts +5 -1
  39. package/dist/formula/node.d.ts.map +1 -1
  40. package/dist/formula/node.js +19 -3
  41. package/dist/formula/node.js.map +1 -1
  42. package/dist/html/controls.d.ts +4 -1
  43. package/dist/html/controls.d.ts.map +1 -1
  44. package/dist/html/controls.js.map +1 -1
  45. package/dist/html/css/cascade.d.ts +11 -5
  46. package/dist/html/css/cascade.d.ts.map +1 -1
  47. package/dist/html/css/cascade.js +18 -8
  48. package/dist/html/css/cascade.js.map +1 -1
  49. package/dist/html/css/parse.d.ts +15 -10
  50. package/dist/html/css/parse.d.ts.map +1 -1
  51. package/dist/html/css/parse.js +41 -16
  52. package/dist/html/css/parse.js.map +1 -1
  53. package/dist/html/css/style.d.ts +3 -0
  54. package/dist/html/css/style.d.ts.map +1 -1
  55. package/dist/html/css/style.js +3 -1
  56. package/dist/html/css/style.js.map +1 -1
  57. package/dist/html/css/values.d.ts +14 -1
  58. package/dist/html/css/values.d.ts.map +1 -1
  59. package/dist/html/css/values.js +11 -8
  60. package/dist/html/css/values.js.map +1 -1
  61. package/dist/html/index.d.ts.map +1 -1
  62. package/dist/html/index.js +1 -0
  63. package/dist/html/index.js.map +1 -1
  64. package/dist/html/layout/boxes.d.ts +12 -5
  65. package/dist/html/layout/boxes.d.ts.map +1 -1
  66. package/dist/html/layout/boxes.js +7 -4
  67. package/dist/html/layout/boxes.js.map +1 -1
  68. package/dist/html/layout/inline.d.ts.map +1 -1
  69. package/dist/html/layout/inline.js +22 -1
  70. package/dist/html/layout/inline.js.map +1 -1
  71. package/dist/html/node.d.ts +22 -8
  72. package/dist/html/node.d.ts.map +1 -1
  73. package/dist/html/node.js +97 -15
  74. package/dist/html/node.js.map +1 -1
  75. package/dist/html/paint.d.ts +3 -0
  76. package/dist/html/paint.d.ts.map +1 -1
  77. package/dist/html/paint.js +2 -2
  78. package/dist/html/paint.js.map +1 -1
  79. package/dist/index.d.ts +11 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +26 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/maps/clip.d.ts +43 -0
  84. package/dist/maps/clip.d.ts.map +1 -0
  85. package/dist/maps/clip.js +171 -0
  86. package/dist/maps/clip.js.map +1 -0
  87. package/dist/maps/gzip.d.ts +15 -0
  88. package/dist/maps/gzip.d.ts.map +1 -0
  89. package/dist/maps/gzip.js +56 -0
  90. package/dist/maps/gzip.js.map +1 -0
  91. package/dist/maps/index.d.ts +55 -0
  92. package/dist/maps/index.d.ts.map +1 -0
  93. package/dist/maps/index.js +141 -0
  94. package/dist/maps/index.js.map +1 -0
  95. package/dist/maps/labels.d.ts +123 -0
  96. package/dist/maps/labels.d.ts.map +1 -0
  97. package/dist/maps/labels.js +323 -0
  98. package/dist/maps/labels.js.map +1 -0
  99. package/dist/maps/mvt.d.ts +244 -0
  100. package/dist/maps/mvt.d.ts.map +1 -0
  101. package/dist/maps/mvt.js +820 -0
  102. package/dist/maps/mvt.js.map +1 -0
  103. package/dist/maps/node.d.ts +278 -0
  104. package/dist/maps/node.d.ts.map +1 -0
  105. package/dist/maps/node.js +1459 -0
  106. package/dist/maps/node.js.map +1 -0
  107. package/dist/maps/overlay.d.ts +157 -0
  108. package/dist/maps/overlay.d.ts.map +1 -0
  109. package/dist/maps/overlay.js +472 -0
  110. package/dist/maps/overlay.js.map +1 -0
  111. package/dist/maps/paint.d.ts +226 -0
  112. package/dist/maps/paint.d.ts.map +1 -0
  113. package/dist/maps/paint.js +705 -0
  114. package/dist/maps/paint.js.map +1 -0
  115. package/dist/maps/proj.d.ts +281 -0
  116. package/dist/maps/proj.d.ts.map +1 -0
  117. package/dist/maps/proj.js +451 -0
  118. package/dist/maps/proj.js.map +1 -0
  119. package/dist/maps/sources.d.ts +256 -0
  120. package/dist/maps/sources.d.ts.map +1 -0
  121. package/dist/maps/sources.js +210 -0
  122. package/dist/maps/sources.js.map +1 -0
  123. package/dist/maps/style.d.ts +141 -0
  124. package/dist/maps/style.d.ts.map +1 -0
  125. package/dist/maps/style.js +147 -0
  126. package/dist/maps/style.js.map +1 -0
  127. package/dist/maps/styles.d.ts +82 -0
  128. package/dist/maps/styles.d.ts.map +1 -0
  129. package/dist/maps/styles.js +898 -0
  130. package/dist/maps/styles.js.map +1 -0
  131. package/dist/maps/tiles.d.ts +283 -0
  132. package/dist/maps/tiles.d.ts.map +1 -0
  133. package/dist/maps/tiles.js +469 -0
  134. package/dist/maps/tiles.js.map +1 -0
  135. package/dist/maps/types.d.ts +244 -0
  136. package/dist/maps/types.d.ts.map +1 -0
  137. package/dist/maps/types.js +2 -0
  138. package/dist/maps/types.js.map +1 -0
  139. package/dist/qml/containers.d.ts +3 -0
  140. package/dist/qml/containers.d.ts.map +1 -0
  141. package/dist/qml/containers.js +331 -0
  142. package/dist/qml/containers.js.map +1 -0
  143. package/dist/qml/controls.d.ts +12 -0
  144. package/dist/qml/controls.d.ts.map +1 -0
  145. package/dist/qml/controls.js +21 -0
  146. package/dist/qml/controls.js.map +1 -0
  147. package/dist/qml/globals.d.ts +6 -0
  148. package/dist/qml/globals.d.ts.map +1 -0
  149. package/dist/qml/globals.js +25 -0
  150. package/dist/qml/globals.js.map +1 -0
  151. package/dist/qml/index.d.ts +15 -0
  152. package/dist/qml/index.d.ts.map +1 -0
  153. package/dist/qml/index.js +25 -0
  154. package/dist/qml/index.js.map +1 -0
  155. package/dist/qml/interaction.d.ts +12 -0
  156. package/dist/qml/interaction.d.ts.map +1 -0
  157. package/dist/qml/interaction.js +246 -0
  158. package/dist/qml/interaction.js.map +1 -0
  159. package/dist/qml/ir.d.ts +106 -0
  160. package/dist/qml/ir.d.ts.map +1 -0
  161. package/dist/qml/ir.js +7 -0
  162. package/dist/qml/ir.js.map +1 -0
  163. package/dist/qml/layouts.d.ts +3 -0
  164. package/dist/qml/layouts.d.ts.map +1 -0
  165. package/dist/qml/layouts.js +122 -0
  166. package/dist/qml/layouts.js.map +1 -0
  167. package/dist/qml/models.d.ts +14 -0
  168. package/dist/qml/models.d.ts.map +1 -0
  169. package/dist/qml/models.js +80 -0
  170. package/dist/qml/models.js.map +1 -0
  171. package/dist/qml/objects.d.ts +228 -0
  172. package/dist/qml/objects.d.ts.map +1 -0
  173. package/dist/qml/objects.js +1103 -0
  174. package/dist/qml/objects.js.map +1 -0
  175. package/dist/qml/parse.d.ts +10 -0
  176. package/dist/qml/parse.d.ts.map +1 -0
  177. package/dist/qml/parse.js +607 -0
  178. package/dist/qml/parse.js.map +1 -0
  179. package/dist/qml/qtquick.d.ts +7 -0
  180. package/dist/qml/qtquick.d.ts.map +1 -0
  181. package/dist/qml/qtquick.js +347 -0
  182. package/dist/qml/qtquick.js.map +1 -0
  183. package/dist/qml/react.d.ts +83 -0
  184. package/dist/qml/react.d.ts.map +1 -0
  185. package/dist/qml/react.js +323 -0
  186. package/dist/qml/react.js.map +1 -0
  187. package/dist/qml/resolver.d.ts +38 -0
  188. package/dist/qml/resolver.d.ts.map +1 -0
  189. package/dist/qml/resolver.js +64 -0
  190. package/dist/qml/resolver.js.map +1 -0
  191. package/dist/qml/slots.d.ts +65 -0
  192. package/dist/qml/slots.d.ts.map +1 -0
  193. package/dist/qml/slots.js +230 -0
  194. package/dist/qml/slots.js.map +1 -0
  195. package/dist/qml/states.d.ts +5 -0
  196. package/dist/qml/states.d.ts.map +1 -0
  197. package/dist/qml/states.js +276 -0
  198. package/dist/qml/states.js.map +1 -0
  199. package/dist/qml/view-utils.d.ts +42 -0
  200. package/dist/qml/view-utils.d.ts.map +1 -0
  201. package/dist/qml/view-utils.js +121 -0
  202. package/dist/qml/view-utils.js.map +1 -0
  203. package/dist/reorder/hx.d.ts +18 -0
  204. package/dist/reorder/hx.d.ts.map +1 -0
  205. package/dist/reorder/hx.js +10 -0
  206. package/dist/reorder/hx.js.map +1 -0
  207. package/dist/reorder/index.d.ts +314 -0
  208. package/dist/reorder/index.d.ts.map +1 -0
  209. package/dist/reorder/index.js +1539 -0
  210. package/dist/reorder/index.js.map +1 -0
  211. package/dist/reorder/model.d.ts +107 -0
  212. package/dist/reorder/model.d.ts.map +1 -0
  213. package/dist/reorder/model.js +162 -0
  214. package/dist/reorder/model.js.map +1 -0
  215. package/dist/richtext/node.d.ts +15 -5
  216. package/dist/richtext/node.d.ts.map +1 -1
  217. package/dist/richtext/node.js +40 -11
  218. package/dist/richtext/node.js.map +1 -1
  219. package/dist/richtext/runs.d.ts +35 -13
  220. package/dist/richtext/runs.d.ts.map +1 -1
  221. package/dist/richtext/runs.js +83 -27
  222. package/dist/richtext/runs.js.map +1 -1
  223. package/dist/tabs/hx.d.ts +18 -0
  224. package/dist/tabs/hx.d.ts.map +1 -0
  225. package/dist/tabs/hx.js +10 -0
  226. package/dist/tabs/hx.js.map +1 -0
  227. package/dist/tabs/index.d.ts +160 -0
  228. package/dist/tabs/index.d.ts.map +1 -0
  229. package/dist/tabs/index.js +1265 -0
  230. package/dist/tabs/index.js.map +1 -0
  231. package/dist/terminal/vt/fonts.d.ts +45 -15
  232. package/dist/terminal/vt/fonts.d.ts.map +1 -1
  233. package/dist/terminal/vt/fonts.js +89 -19
  234. package/dist/terminal/vt/fonts.js.map +1 -1
  235. package/dist/terminal/vt/index.d.ts +1 -1
  236. package/dist/terminal/vt/index.d.ts.map +1 -1
  237. package/dist/terminal/vt/index.js +3 -3
  238. package/dist/terminal/vt/index.js.map +1 -1
  239. package/dist/terminal/vt/node.d.ts +42 -0
  240. package/dist/terminal/vt/node.d.ts.map +1 -1
  241. package/dist/terminal/vt/node.js +95 -11
  242. package/dist/terminal/vt/node.js.map +1 -1
  243. package/dist/terminal/vt/pty.d.ts +16 -0
  244. package/dist/terminal/vt/pty.d.ts.map +1 -1
  245. package/dist/terminal/vt/pty.js +186 -0
  246. package/dist/terminal/vt/pty.js.map +1 -1
  247. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  248. package/dist/terminal/vt/renderer.js +13 -3
  249. package/dist/terminal/vt/renderer.js.map +1 -1
  250. package/package.json +23 -3
  251. package/src/charts/components.ts +14 -7
  252. package/src/charts/node.ts +81 -26
  253. package/src/charts/render.ts +16 -6
  254. package/src/code-editor/node.ts +147 -53
  255. package/src/flow/draw.ts +104 -35
  256. package/src/flow/index.ts +33 -13
  257. package/src/flow/model.ts +4 -2
  258. package/src/flow/node.ts +158 -59
  259. package/src/flow/types.ts +46 -21
  260. package/src/formula/index.ts +4 -2
  261. package/src/formula/node.ts +24 -5
  262. package/src/html/controls.ts +4 -1
  263. package/src/html/css/cascade.ts +18 -7
  264. package/src/html/css/parse.ts +46 -21
  265. package/src/html/css/style.ts +6 -1
  266. package/src/html/css/values.ts +25 -9
  267. package/src/html/index.ts +1 -0
  268. package/src/html/layout/boxes.ts +19 -9
  269. package/src/html/layout/inline.ts +23 -1
  270. package/src/html/node.ts +110 -17
  271. package/src/html/paint.ts +17 -2
  272. package/src/index.ts +142 -0
  273. package/src/maps/clip.ts +184 -0
  274. package/src/maps/gzip.ts +66 -0
  275. package/src/maps/index.ts +316 -0
  276. package/src/maps/labels.ts +442 -0
  277. package/src/maps/mvt.ts +857 -0
  278. package/src/maps/node.ts +1819 -0
  279. package/src/maps/overlay.ts +639 -0
  280. package/src/maps/paint.ts +978 -0
  281. package/src/maps/proj.ts +641 -0
  282. package/src/maps/sources.ts +425 -0
  283. package/src/maps/style.ts +309 -0
  284. package/src/maps/styles.ts +1040 -0
  285. package/src/maps/tiles.ts +676 -0
  286. package/src/maps/types.ts +260 -0
  287. package/src/qml/containers.tsx +380 -0
  288. package/src/qml/controls.tsx +47 -0
  289. package/src/qml/globals.ts +33 -0
  290. package/src/qml/index.ts +67 -0
  291. package/src/qml/interaction.tsx +317 -0
  292. package/src/qml/ir.ts +80 -0
  293. package/src/qml/layouts.tsx +141 -0
  294. package/src/qml/models.ts +101 -0
  295. package/src/qml/objects.ts +1431 -0
  296. package/src/qml/parse.ts +624 -0
  297. package/src/qml/qtquick.tsx +413 -0
  298. package/src/qml/react.tsx +454 -0
  299. package/src/qml/resolver.ts +99 -0
  300. package/src/qml/slots.ts +249 -0
  301. package/src/qml/states.ts +318 -0
  302. package/src/qml/view-utils.ts +140 -0
  303. package/src/reorder/hx.ts +31 -0
  304. package/src/reorder/index.ts +2223 -0
  305. package/src/reorder/model.ts +232 -0
  306. package/src/richtext/node.ts +49 -14
  307. package/src/richtext/runs.ts +115 -30
  308. package/src/tabs/hx.ts +31 -0
  309. package/src/tabs/index.ts +1686 -0
  310. package/src/terminal/vt/fonts.ts +128 -36
  311. package/src/terminal/vt/index.ts +4 -2
  312. package/src/terminal/vt/node.ts +105 -12
  313. package/src/terminal/vt/pty.ts +243 -0
  314. package/src/terminal/vt/renderer.ts +13 -3
@@ -0,0 +1,454 @@
1
+ // The React bridge: one small component per live QML object.
2
+ //
3
+ // Each QmlNode subscribes to exactly one instance (useSyncExternalStore on
4
+ // the instance's version counter), so a scalar property change re-renders
5
+ // one element's worth of tree and React reconciliation is reserved for
6
+ // structural change (Repeater rebuilds). The QML side never reaches around
7
+ // React: every pixel on screen is a `<box>`/`<text>`/`<image>` committed
8
+ // through the ordinary renderer.
9
+
10
+ import {
11
+ forwardRef,
12
+ useEffect,
13
+ useImperativeHandle,
14
+ useRef,
15
+ useSyncExternalStore,
16
+ } from 'react';
17
+ import type { ComponentType, ReactElement, ReactNode, Ref } from 'react';
18
+ import type { DrawnNode } from 'react-x11';
19
+ import type { Style } from 'react-x11/style';
20
+ import { warn } from './globals.js';
21
+
22
+ import { parseQml } from './parse.js';
23
+ import {
24
+ Context,
25
+ QmlInstance,
26
+ instantiateDocument,
27
+ migrateUserState,
28
+ registerQmlModule,
29
+ type InstantiateResult,
30
+ type QmlFacade,
31
+ type QmlPropertyDef,
32
+ } from './objects.js';
33
+ import { flushBindings } from './slots.js';
34
+ import { afterLayout, cancelAfterLayout } from '../internal/timers.js';
35
+ import type { QmlResolver } from './resolver.js';
36
+
37
+ export function useQmlVersion(inst: QmlInstance): number {
38
+ return useSyncExternalStore(
39
+ (cb) => inst.subscribe(cb),
40
+ () => inst.version,
41
+ );
42
+ }
43
+
44
+ export function QmlNode({ inst }: { inst: QmlInstance }): ReactNode {
45
+ useQmlVersion(inst);
46
+ if (inst.destroyed) return null;
47
+ const View = inst.typeInfo.view;
48
+ if (!View) return null;
49
+ return <View inst={inst} />;
50
+ }
51
+
52
+ export function renderQmlChildren(inst: QmlInstance): ReactNode[] {
53
+ return inst.visualChildren().map((c) => <QmlNode key={c.uid} inst={c} />);
54
+ }
55
+
56
+ export interface QmlViewHandle {
57
+ /** The root object, as expressions see it: live properties, callable
58
+ * signals and methods. */
59
+ root: QmlFacade;
60
+ /** An object by its document id, or null. */
61
+ id(name: string): QmlFacade | null;
62
+ instance: QmlInstance;
63
+ }
64
+
65
+ export interface QmlViewProps {
66
+ /** QML source text. When it changes the tree is rebuilt; interactive
67
+ * state (assigned property values) is carried across by id — the
68
+ * hot-reload story. Pass `hotReload={false}` for a cold swap. */
69
+ source: string;
70
+ /** Shown in errors; pass the file path when the source came from one. */
71
+ file?: string;
72
+ /** Context properties — names visible to every binding in the document.
73
+ * Updated values ride ordinary commits. */
74
+ context?: Record<string, unknown>;
75
+ /** Subscriptions to the root object's signals, by signal name. */
76
+ onSignal?: Record<string, (...args: unknown[]) => void>;
77
+ hotReload?: boolean;
78
+ /** Enables `.qml`-file resolution — the implicit same-directory import
79
+ * and quoted-path imports. `createFileResolver(dir)` is the standard
80
+ * filesystem one; any object with the `QmlResolver` shape works
81
+ * (resolver.ts). Fixed for the lifetime of the view. */
82
+ resolver?: QmlResolver;
83
+ /** Rebuild (with the same state migration a `source` change gets) when
84
+ * this value changes — how a watcher of *sibling* `.qml` files triggers
85
+ * a hot reload the unchanged root source cannot. */
86
+ reloadToken?: unknown;
87
+ style?: Style | Style[];
88
+ }
89
+
90
+ interface Mounted {
91
+ source: string;
92
+ token: unknown;
93
+ res: InstantiateResult;
94
+ }
95
+
96
+ /**
97
+ * A QML document as a react-x11 element tree.
98
+ *
99
+ * The wrapper box takes the root object's declared size when it has one;
100
+ * otherwise it is sized by the app (the `style` prop) and feeds its
101
+ * laid-out size back into the root's implicit size, so `width`/`height`
102
+ * bindings inside the document see the real number — the "root fills the
103
+ * window" behavior.
104
+ */
105
+ export const QmlView = forwardRef(function QmlView(
106
+ {
107
+ source,
108
+ file,
109
+ context,
110
+ onSignal,
111
+ hotReload = true,
112
+ resolver,
113
+ reloadToken,
114
+ style,
115
+ }: QmlViewProps,
116
+ ref: Ref<QmlViewHandle>,
117
+ ): ReactElement {
118
+ const state = useRef<Mounted | null>(null);
119
+ if (
120
+ !state.current ||
121
+ state.current.source !== source ||
122
+ !Object.is(state.current.token, reloadToken)
123
+ ) {
124
+ const doc = parseQml(source, { fileName: file ?? '<inline>' });
125
+ const next = instantiateDocument(doc, {
126
+ extras: { ...context },
127
+ resolver: resolver ?? null,
128
+ });
129
+ if (state.current) {
130
+ if (hotReload) migrateUserState(state.current.res.context, next.context);
131
+ state.current.res.root.destroy();
132
+ }
133
+ state.current = { source, token: reloadToken, res: next };
134
+ }
135
+ const { res } = state.current;
136
+ const root = res.root;
137
+
138
+ useQmlVersion(root); // the wrapper box tracks the root's size
139
+
140
+ // Context-property updates ride ordinary commits.
141
+ useEffect(() => {
142
+ if (!context) return;
143
+ for (const [k, v] of Object.entries(context)) {
144
+ const slot = res.context.extras.get(k);
145
+ if (slot && !Object.is(slot.peek(), v)) slot.assign(v);
146
+ }
147
+ flushBindings();
148
+ });
149
+
150
+ useEffect(() => {
151
+ if (!onSignal) return;
152
+ const offs = Object.entries(onSignal).map(([name, fn]) =>
153
+ root.onSignal(name, fn),
154
+ );
155
+ return () => offs.forEach((off) => off());
156
+ }, [root, onSignal]);
157
+
158
+ useEffect(() => {
159
+ const mounted = state.current;
160
+ return () => {
161
+ mounted?.res.root.destroy();
162
+ if (state.current === mounted) state.current = null;
163
+ };
164
+ }, []);
165
+
166
+ useImperativeHandle(
167
+ ref,
168
+ () => ({
169
+ root: root.facade,
170
+ id: (name: string) => res.context.ids.get(name)?.facade ?? null,
171
+ instance: root,
172
+ }),
173
+ [root, res],
174
+ );
175
+
176
+ // Root-size feedback. `afterLayout` (not a bare effect): react-x11 lays
177
+ // out on the frame flush, so an effect body reads the geometry of the
178
+ // previous pass — src/internal/timers.ts documents the shape.
179
+ const wrapRef = useRef<DrawnNode>(null);
180
+ const rootW = root.slots.get('width');
181
+ const rootH = root.slots.get('height');
182
+ const sized = !!rootW && !rootW.isDefault;
183
+ useEffect(() => {
184
+ if (sized) return;
185
+ const tick = afterLayout(() => {
186
+ const abs = wrapRef.current?.abs;
187
+ if (!abs || root.destroyed) return;
188
+ if (abs.width) root.slots.get('implicitWidth')?.assign(abs.width);
189
+ if (abs.height) root.slots.get('implicitHeight')?.assign(abs.height);
190
+ flushBindings();
191
+ });
192
+ return () => cancelAfterLayout(tick);
193
+ });
194
+
195
+ return (
196
+ <box
197
+ ref={wrapRef}
198
+ style={[
199
+ { position: 'relative' },
200
+ sized && rootH
201
+ ? { width: num(rootW.peek()), height: num(rootH.peek()) }
202
+ : false,
203
+ style,
204
+ ]}
205
+ >
206
+ <QmlNode inst={root} />
207
+ </box>
208
+ );
209
+ });
210
+
211
+ const num = (v: unknown): number =>
212
+ typeof v === 'number' && Number.isFinite(v) ? v : 0;
213
+
214
+ export interface RegisterReactComponentOptions {
215
+ properties?: Record<string, QmlPropertyDef>;
216
+ signals?: Record<string, string[]>;
217
+ /** Reshape when prop names don't line up one-to-one. */
218
+ toProps?: (
219
+ props: Record<string, unknown>,
220
+ inst: QmlInstance,
221
+ ) => Record<string, unknown>;
222
+ }
223
+
224
+ /**
225
+ * Register a React component as a QML type — the "extend in code" seam.
226
+ *
227
+ * registerReactComponent('Shop.Widgets', 'PriceTag', PriceTag, {
228
+ * properties: { amount: { default: 0 } },
229
+ * signals: { activated: [] },
230
+ * });
231
+ *
232
+ * Declared properties arrive as props (live: a binding update re-renders
233
+ * the component); declared signals arrive as `on<Signal>` callback props
234
+ * that emit back into QML.
235
+ */
236
+ export function registerReactComponent(
237
+ moduleName: string,
238
+ typeName: string,
239
+ Component: ComponentType<Record<string, unknown>>,
240
+ {
241
+ properties = {},
242
+ signals = {},
243
+ toProps,
244
+ }: RegisterReactComponentOptions = {},
245
+ ): void {
246
+ const View = ({ inst }: { inst: QmlInstance }): ReactElement => {
247
+ const props: Record<string, unknown> = {};
248
+ for (const name of Object.keys(properties)) {
249
+ props[name] = inst.slots.get(name)?.peek();
250
+ }
251
+ for (const name of Object.keys(signals)) {
252
+ props[`on${name[0].toUpperCase()}${name.slice(1)}`] = (
253
+ ...args: unknown[]
254
+ ) => inst.emit(name, ...args);
255
+ }
256
+ const shaped = toProps ? toProps(props, inst) : props;
257
+ const kids = renderQmlChildren(inst);
258
+ return (
259
+ <box style={geometryStyle(inst)}>
260
+ <Component {...shaped}>
261
+ {kids.length ? kids : (shaped.children as ReactNode)}
262
+ </Component>
263
+ </box>
264
+ );
265
+ };
266
+ registerQmlModule(moduleName, {
267
+ types: {
268
+ [typeName]: { extends: 'Item', properties, signals, view: View },
269
+ },
270
+ });
271
+ }
272
+
273
+ // Shared with qtquick.tsx (kept here so registerReactComponent has no
274
+ // dependency on the QtQuick module being loaded).
275
+ export function geometryStyle(
276
+ inst: QmlInstance,
277
+ { contentSized = false } = {},
278
+ ): Style {
279
+ // Inside a RowLayout/ColumnLayout the item is a flex item: yoga decides
280
+ // its geometry from the Layout.* attached properties, and the container
281
+ // reflects the answers back into x/y/width/height afterwards.
282
+ if (inst.parentInst?.typeInfo.managesChildLayout) {
283
+ return layoutChildStyle(inst);
284
+ }
285
+ const s = inst.slots;
286
+ const style: Style = {
287
+ position: 'absolute',
288
+ left: num(s.get('x')?.peek()),
289
+ top: num(s.get('y')?.peek()),
290
+ };
291
+ const w = s.get('width');
292
+ const h = s.get('height');
293
+ if (w && !(contentSized && w.isDefault)) style.width = num(w.peek());
294
+ if (h && !(contentSized && h.isDefault)) style.height = num(h.peek());
295
+ if (s.get('visible')?.peek() === false) style.display = 'none';
296
+ if (s.get('clip')?.peek() === true) style.overflow = 'hidden';
297
+ const z = s.get('z')?.peek();
298
+ if (typeof z === 'number' && z) style.zIndex = z;
299
+ warnUnrenderable(inst);
300
+ return style;
301
+ }
302
+
303
+ // --- QtQuick.Layouts: the flex-item half -----------------------------------
304
+
305
+ // Qt.AlignmentFlag values (the subset Layout.alignment uses).
306
+ const ALIGN = {
307
+ left: 1,
308
+ right: 2,
309
+ hcenter: 4,
310
+ top: 32,
311
+ bottom: 64,
312
+ vcenter: 128,
313
+ } as const;
314
+
315
+ const layoutNumOf = (v: unknown): number | null =>
316
+ typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
317
+
318
+ /**
319
+ * An item's size hint on one axis, in Qt's order: `Layout.preferredWidth`,
320
+ * then an author-set `width` (friendlier than Qt, which ignores it — ours
321
+ * reads as a preferred size), then `implicitWidth`. `read` decides
322
+ * tracking: `.get` inside the container's implicit-size bindings, `.peek`
323
+ * during render.
324
+ */
325
+ export function preferredSpan(
326
+ inst: QmlInstance,
327
+ horizontal: boolean,
328
+ read: (slot: NonNullable<ReturnType<QmlInstance['slots']['get']>>) => unknown,
329
+ filling = false,
330
+ ): number | null {
331
+ const pref = inst.slots.get(
332
+ horizontal ? 'Layout.preferredWidth' : 'Layout.preferredHeight',
333
+ );
334
+ if (pref) {
335
+ const v = layoutNumOf(read(pref));
336
+ if (v !== null) return v;
337
+ }
338
+ if (!filling) {
339
+ const own = inst.slots.get(horizontal ? 'width' : 'height');
340
+ if (own && !own.isDefault) {
341
+ const v = layoutNumOf(read(own));
342
+ if (v !== null && v > 0) return v;
343
+ }
344
+ }
345
+ const implicit = inst.slots.get(
346
+ horizontal ? 'implicitWidth' : 'implicitHeight',
347
+ );
348
+ if (implicit) {
349
+ const v = layoutNumOf(read(implicit));
350
+ if (v !== null && v > 0) return v;
351
+ }
352
+ return null;
353
+ }
354
+
355
+ function layoutChildStyle(inst: QmlInstance): Style {
356
+ const parent = inst.parentInst;
357
+ const horizontal = parent?.typeInfo.name === 'RowLayout';
358
+ const style: Style = {};
359
+ const la = (name: string): unknown =>
360
+ inst.slots.get(`Layout.${name}`)?.peek();
361
+ const peek = (slot: { peek(): unknown }): unknown => slot.peek();
362
+
363
+ const fillMain = la(horizontal ? 'fillWidth' : 'fillHeight') === true;
364
+ const fillCross = la(horizontal ? 'fillHeight' : 'fillWidth') === true;
365
+ const stretchRaw = la(
366
+ horizontal ? 'horizontalStretchFactor' : 'verticalStretchFactor',
367
+ );
368
+ const stretch = layoutNumOf(stretchRaw) ?? 0;
369
+
370
+ // Main axis: preferred size is the flex basis; `fillWidth` grows.
371
+ const basis = preferredSpan(inst, horizontal, peek, fillMain);
372
+ if (fillMain) {
373
+ style.flexGrow = stretch > 0 ? stretch : 1;
374
+ style.flexBasis = basis ?? 0;
375
+ } else {
376
+ style.flexGrow = 0;
377
+ style.flexShrink = 0;
378
+ if (basis !== null) {
379
+ if (horizontal) style.width = basis;
380
+ else style.height = basis;
381
+ }
382
+ }
383
+
384
+ // Cross axis: fill stretches; otherwise the preferred size holds and
385
+ // `Layout.alignment` places the item (Qt's defaults: vertically centered
386
+ // in a row, left in a column).
387
+ const crossPref = preferredSpan(inst, !horizontal, peek, fillCross);
388
+ if (fillCross) {
389
+ style.alignSelf = 'stretch';
390
+ } else {
391
+ if (crossPref !== null) {
392
+ if (horizontal) style.height = crossPref;
393
+ else style.width = crossPref;
394
+ }
395
+ const alignment = layoutNumOf(la('alignment')) ?? 0;
396
+ const startFlag = horizontal ? ALIGN.top : ALIGN.left;
397
+ const endFlag = horizontal ? ALIGN.bottom : ALIGN.right;
398
+ const centerFlag = horizontal ? ALIGN.vcenter : ALIGN.hcenter;
399
+ style.alignSelf =
400
+ alignment & startFlag
401
+ ? 'flex-start'
402
+ : alignment & endFlag
403
+ ? 'flex-end'
404
+ : alignment & centerFlag
405
+ ? 'center'
406
+ : horizontal
407
+ ? 'center'
408
+ : 'flex-start';
409
+ }
410
+
411
+ const minW = layoutNumOf(la('minimumWidth'));
412
+ if (minW !== null) style.minWidth = minW;
413
+ const minH = layoutNumOf(la('minimumHeight'));
414
+ if (minH !== null) style.minHeight = minH;
415
+ const maxW = layoutNumOf(la('maximumWidth'));
416
+ if (maxW !== null) style.maxWidth = maxW;
417
+ const maxH = layoutNumOf(la('maximumHeight'));
418
+ if (maxH !== null) style.maxHeight = maxH;
419
+
420
+ const margins = layoutNumOf(la('margins')) ?? 0;
421
+ const side = (name: string): number => layoutNumOf(la(name)) ?? margins;
422
+ if (margins || la('leftMargin') !== undefined) {
423
+ style.marginLeft = side('leftMargin');
424
+ }
425
+ if (margins || la('rightMargin') !== undefined)
426
+ style.marginRight = side('rightMargin');
427
+ if (margins || la('topMargin') !== undefined)
428
+ style.marginTop = side('topMargin');
429
+ if (margins || la('bottomMargin') !== undefined)
430
+ style.marginBottom = side('bottomMargin');
431
+
432
+ if (inst.slots.get('visible')?.peek() === false) style.display = 'none';
433
+ if (inst.slots.get('clip')?.peek() === true) style.overflow = 'hidden';
434
+ warnUnrenderable(inst);
435
+ return style;
436
+ }
437
+
438
+ const INERT: Record<string, number> = { opacity: 1, rotation: 0, scale: 1 };
439
+ const warned = new Set<string>();
440
+ function warnUnrenderable(inst: QmlInstance): void {
441
+ for (const name of Object.keys(INERT)) {
442
+ const v = inst.slots.get(name)?.peek();
443
+ if (v !== undefined && v !== INERT[name] && !warned.has(name)) {
444
+ warned.add(name);
445
+ warn(
446
+ `QML: '${name}' is declared but react-x11 cannot render it yet ` +
447
+ `(needs offscreen composition); the value is ignored.`,
448
+ );
449
+ }
450
+ }
451
+ }
452
+
453
+ // Re-exported here so the family's public surface sits in one place.
454
+ export { Context, QmlInstance };
@@ -0,0 +1,99 @@
1
+ // The resolver seam: how `.qml` files find each other.
2
+ //
3
+ // The engine never touches a filesystem — it asks a `QmlResolver` for
4
+ // component sources and treats directory strings as opaque tokens only the
5
+ // resolver interprets. That keeps the engine host-neutral (the headless
6
+ // tests hand it plain objects) and makes "where do components come from"
7
+ // an app decision: a directory on disk, a bundle, an HTTP cache, a test
8
+ // fixture.
9
+ //
10
+ // Three ways a type name resolves, in Qt's precedence order:
11
+ // 1. the *implicit import* — `<Name>.qml` beside the document (no import
12
+ // line at all, and a local `Button.qml` shadows a module's `Button`);
13
+ // 2. quoted-path imports — `import "./widgets"` brings in that
14
+ // directory's components;
15
+ // 3. registered modules — `import QtQuick 2.15` and anything an app adds
16
+ // with `registerQmlModule`.
17
+ // The first two exist only when a resolver is provided.
18
+
19
+ export interface QmlResolver {
20
+ /** Where the root document lives — the starting directory for its
21
+ * implicit import. Opaque to the engine. */
22
+ rootDir: string;
23
+ /** The source of `<name>.qml` in `dir`, or null when there is no such
24
+ * component. Called often (every type name is probed here first, in
25
+ * Qt's shadowing order) but cached by the engine per instantiation —
26
+ * a plain `existsSync` + `readFileSync` implementation is fine. */
27
+ load(dir: string, name: string): { source: string; fileName: string } | null;
28
+ /** A quoted relative import (`"./widgets"`, `"../shared"`) resolved
29
+ * against a document's directory. */
30
+ join(dir: string, relative: string): string;
31
+ }
32
+
33
+ /** How a document was loaded — carried on `QmlDocument` so the types it
34
+ * names can resolve relative to it. */
35
+ export interface DocLoad {
36
+ resolver: QmlResolver;
37
+ dir: string;
38
+ }
39
+
40
+ // --- the standard filesystem resolver --------------------------------------
41
+
42
+ interface FsModule {
43
+ existsSync(path: string): boolean;
44
+ readFileSync(path: string, encoding: 'utf8'): string;
45
+ }
46
+
47
+ interface PathModule {
48
+ resolve(...parts: string[]): string;
49
+ join(...parts: string[]): string;
50
+ }
51
+
52
+ /**
53
+ * Load a node builtin without naming it to the compiler — `src/` compiles
54
+ * with `types: []` on purpose, and a literal `import('node:fs')` would
55
+ * need `@types/node` in the build program (the `embed/host.ts` pattern).
56
+ */
57
+ const builtins = new Map<string, Promise<unknown>>();
58
+ function builtin<T>(name: string): Promise<T> {
59
+ let mod = builtins.get(name);
60
+ if (!mod) {
61
+ const specifier = name;
62
+ mod = import(/* @vite-ignore */ specifier);
63
+ builtins.set(name, mod);
64
+ }
65
+ return mod as Promise<T>;
66
+ }
67
+
68
+ /**
69
+ * The standard filesystem resolver — pass the directory your root document
70
+ * lives in:
71
+ *
72
+ * const resolver = await createFileResolver(dirname(qmlPath));
73
+ * <QmlView source={source} file={qmlPath} resolver={resolver} />
74
+ *
75
+ * Async because the node builtins load through a dynamic import (the build
76
+ * has no node types); the resolver it returns is fully synchronous. It
77
+ * reads through on every engine request — no content cache — so a changed
78
+ * sibling file is picked up on the next root reload; the engine's own
79
+ * per-instantiation cache keeps a 1000-delegate ListView from re-reading
80
+ * its component 1000 times.
81
+ */
82
+ export async function createFileResolver(
83
+ baseDir: string,
84
+ ): Promise<QmlResolver> {
85
+ const fs = await builtin<FsModule>('node:fs');
86
+ const path = await builtin<PathModule>('node:path');
87
+ const rootDir = path.resolve(baseDir);
88
+ return {
89
+ rootDir,
90
+ load(dir, name) {
91
+ const file = path.join(dir, `${name}.qml`);
92
+ if (!fs.existsSync(file)) return null;
93
+ return { source: fs.readFileSync(file, 'utf8'), fileName: file };
94
+ },
95
+ join(dir, relative) {
96
+ return path.resolve(dir, relative);
97
+ },
98
+ };
99
+ }