quickwin 1.0.1 → 1.1.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 (79) hide show
  1. package/README.en.md +204 -204
  2. package/README.md +203 -203
  3. package/examples/PdfCanvas.tsx +83 -83
  4. package/examples/exec_server.js +25 -3
  5. package/examples/exec_server.ts +35 -17
  6. package/examples/pdf_preview2.ts +375 -375
  7. package/examples/pdf_viewer.tsx +279 -279
  8. package/examples/test_gallery.tsx +285 -285
  9. package/examples/test_react_lazy.tsx +63 -63
  10. package/examples/test_react_listbox.tsx +71 -71
  11. package/examples/test_root_window.tsx +41 -41
  12. package/examples/test_tab.tsx +75 -75
  13. package/examples/tray_demo.tsx +81 -81
  14. package/lib/assert.ts +6 -6
  15. package/lib/cache-utils.ts +40 -40
  16. package/lib/dumpRects.ts +25 -25
  17. package/lib/fetch.ts +502 -502
  18. package/lib/http-common.ts +399 -399
  19. package/lib/http-server.ts +424 -424
  20. package/lib/polyfill.ts +171 -171
  21. package/lib/react-qw/components/index.ts +17 -17
  22. package/lib/react-qw/index.ts +90 -90
  23. package/lib/react-qw/jsx.d.ts +34 -34
  24. package/lib/react-qw/layout.ts +111 -111
  25. package/lib/react-qw/props.ts +36 -36
  26. package/lib/react-qw/reconciler.ts +373 -373
  27. package/lib/stream.ts +21 -21
  28. package/lib/text-codec.ts +171 -171
  29. package/lib/text-measure.ts +54 -54
  30. package/lib/url.ts +201 -201
  31. package/lib/vendor/web-streams/ponyfill.d.mts +829 -829
  32. package/lib/vendor/web-streams/ponyfill.mjs +8 -8
  33. package/lib/websocket.js +8 -0
  34. package/lib/websocket.ts +563 -555
  35. package/package.json +50 -44
  36. package/qwin-nowasm-x86.exe +0 -0
  37. package/qwin-nowasm.exe +0 -0
  38. package/qwin-x86.exe +0 -0
  39. package/qwin.exe +0 -0
  40. package/test/run.ts +96 -96
  41. package/test/test_basic.ts +9 -9
  42. package/test/test_basic_fetch.ts +33 -33
  43. package/test/test_brotli.ts +52 -52
  44. package/test/test_fetch_cache.ts +187 -187
  45. package/test/test_ffi.ts +174 -174
  46. package/test/test_frame_encoding.ts +132 -132
  47. package/test/test_helper.ts +80 -80
  48. package/test/test_http_import.ts +74 -74
  49. package/test/test_http_server.ts +136 -136
  50. package/test/test_import_dir.ts +5 -5
  51. package/test/test_ipv6.ts +129 -129
  52. package/test/test_meta_url.ts +4 -4
  53. package/test/test_mupdf_render.ts +74 -74
  54. package/test/test_mupdf_twice.ts +81 -81
  55. package/test/test_mupdf_wasm.ts +30 -30
  56. package/test/test_net_event.ts +55 -55
  57. package/test/test_net_fetch.ts +238 -238
  58. package/test/test_net_websocket.ts +164 -164
  59. package/test/test_polyfill.ts +159 -159
  60. package/test/test_url.ts +183 -183
  61. package/test/test_wasm_basic.ts +73 -73
  62. package/test/test_wasm_import_global.ts +45 -45
  63. package/test/test_wasm_sjlj.ts +125 -125
  64. package/test/test_wasm_types.ts +124 -124
  65. package/test/test_worker.ts +24 -24
  66. package/test/test_worker_fetch.ts +27 -27
  67. package/test/test_worker_net.ts +54 -54
  68. package/test/test_worker_wasm_concurrent.ts +45 -45
  69. package/test/worker_fetch.ts +13 -13
  70. package/test/worker_helper.ts +1 -1
  71. package/test/worker_http.ts +20 -20
  72. package/test/worker_import_lib.ts +13 -13
  73. package/test/worker_module.ts +17 -17
  74. package/test/worker_wasm_concurrent.ts +22 -22
  75. package/tsconfig.json +27 -27
  76. package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -571
  77. package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -2749
  78. package/vendor/mupdf-wasm/mupdf.d.ts +939 -939
  79. package/vendor/mupdf-wasm/mupdf.js +3317 -3317
@@ -1,373 +1,373 @@
1
- import ReactReconciler from 'react-reconciler'
2
- import { createContext } from 'react'
3
- import { DefaultEventPriority, NoEventPriority } from 'react-reconciler/constants'
4
- import * as gui from 'gui'
5
- import * as os from 'os'
6
- import { applyProps } from './props.js'
7
- import { calculateFlexLayout, type FlexStyle } from './layout.js'
8
- import { getButtonIdealSize, measureTextForHwnd } from '../text-measure.js'
9
- import type { WIntrinsicProps } from './jsx.js'
10
-
11
- // DEBUG 由 esbuild --define 在 bundle 时替换(见 build.ts),生产环境为 false
12
- declare const DEBUG: boolean
13
-
14
- const dpiFont = gui.CreateSystemDpiFont()
15
- export const scaleFactor = gui.GetScaleFactor()
16
-
17
- type Container = gui.HWND
18
- export type Props = WIntrinsicProps
19
-
20
- export interface Instance {
21
- hwnd: gui.HWND | null
22
- type: string
23
- props: Props
24
- children: Instance[]
25
- lastRect?: { x: number; y: number; w: number; h: number }
26
- }
27
-
28
- const DELAYED_CONTROLS = new Set(['SysListView32', 'SysTreeView32', 'LISTBOX', 'COMBOBOX', 'msctls_trackbar32', 'SysDateTimePick32', 'SysLink'])
29
-
30
- function isDelayedControl(winClass: string): boolean {
31
- return DELAYED_CONTROLS.has(winClass)
32
- }
33
-
34
- function setupWindowProc(instance: Instance, hwnd: gui.HWND) {
35
- instance.hwnd = hwnd
36
- const oldProc = gui.GetWindowLongPtr(hwnd, gui.Gwlp.WNDPROC)
37
- instancesByHwnd.set(hwnd, instance)
38
- gui.SetWindowProc(hwnd, (hwnd: gui.HWND, msg: number, wParam: number, lParam: number) => {
39
- const ev = instance.props.onEvent
40
- if (!ev) return gui.CallWindowProc(oldProc, hwnd, msg, wParam, lParam)
41
- if (typeof ev === 'object') {
42
- return ev.fn({ hwnd, msg, wParam, lParam, callOldWndProc: () => gui.CallWindowProc(oldProc, hwnd, msg, wParam, lParam) })
43
- }
44
- const result = gui.CallWindowProc(oldProc, hwnd, msg, wParam, lParam)
45
- const override = ev({ hwnd, msg, wParam, lParam })
46
- return typeof override === 'number' && Number.isInteger(override) ? override : result
47
- })
48
- }
49
-
50
- function ensureChildWindow(child: Instance, parentHwnd: gui.HWND): gui.HWND {
51
- if (child.hwnd !== null) return child.hwnd
52
- const sty = child.props.style || {}
53
- const ws = ((child.props.ws ?? 0) | gui.WindowStyle.CHILD) & ~gui.WindowStyle.VISIBLE
54
- const hwnd = gui.CreateWindow(
55
- child.type, child.props.text || '', ws,
56
- 0, 0,
57
- (typeof sty.width === 'number' ? sty.width : 100) * scaleFactor, (typeof sty.height === 'number' ? sty.height : 30) * scaleFactor,
58
- parentHwnd, null
59
- )!
60
- if (dpiFont) gui.SendMessage(hwnd, gui.WmMsg.SETFONT, dpiFont, 1)
61
- setupWindowProc(child, hwnd)
62
- return hwnd
63
- }
64
-
65
- type HostContext = Record<string, never>
66
-
67
- export const instancesByHwnd = new Map<gui.HWND, Instance>()
68
-
69
- function runFlexLayout(inst: Instance) {
70
- const children = inst.children
71
- if (children.length === 0) return
72
- const s = inst.props.style
73
- const flex: FlexStyle = s || {}
74
- const visible = children.filter(c => !c.props.hidden)
75
- if (visible.length === 0) return
76
- const rect = gui.GetClientRect(inst.hwnd!)
77
- if (!rect) { console.log('flex: no rect for', inst.hwnd, inst.type); return }
78
- const pw = (rect.right - rect.left) / scaleFactor
79
- const ph = (rect.bottom - rect.top) / scaleFactor
80
- if (pw <= 0 || ph <= 0) { console.log('flex: zero size for', inst.hwnd, inst.type, pw, ph); return }
81
- const autoSizes = new Map<Instance, { w: number; h: number }>()
82
- for (const child of visible) {
83
- const sty = child.props.style || {}
84
- if (!child.hwnd) continue
85
- if (child.type === 'BUTTON' && (sty.width === 'auto' || sty.height === 'auto')) {
86
- const ideal = getButtonIdealSize(child.hwnd)
87
- autoSizes.set(child, { w: ideal.width / scaleFactor, h: ideal.height / scaleFactor })
88
- } else if (child.type === 'STATIC' && (sty.width === 'auto' || sty.height === 'auto') && child.props.text) {
89
- const measured = measureTextForHwnd(child.hwnd, child.props.text!)
90
- autoSizes.set(child, { w: measured.width / scaleFactor, h: measured.height / scaleFactor })
91
- }
92
- }
93
- const results = calculateFlexLayout(flex, pw, ph, visible.map(c => {
94
- const sty = c.props.style || {}
95
- const auto = autoSizes.get(c)
96
- return {
97
- style: {
98
- ...sty,
99
- width: auto ? (sty.width === 'auto' ? auto.w : sty.width) : sty.width,
100
- height: auto ? (sty.height === 'auto' ? auto.h : sty.height) : sty.height,
101
- }
102
- }
103
- }))
104
- const pl = flex.paddingLeft ?? flex.padding ?? 0
105
- const pt = flex.paddingTop ?? flex.padding ?? 0
106
- for (let i = 0; i < results.length; i++) {
107
- const r = results[i]!
108
- const child = visible[i]!
109
- const lr = child.lastRect
110
- if (lr && lr.x === r.x && lr.y === r.y && lr.w === r.width && lr.h === r.height) continue
111
- console.log('flex: set', child.type, child.hwnd, 'to', r.x, r.y, r.width, r.height)
112
- gui.SetWindowPos(child.hwnd!, gui.SetWindowPosHwnd.TOP, (r.x + pl) * scaleFactor, (r.y + pt) * scaleFactor, r.width * scaleFactor, r.height * scaleFactor, gui.SetWindowPosFlag.SWP_SHOWWINDOW)
113
- gui.InvalidateRect(child.hwnd!, null, true)
114
- child.lastRect = { x: r.x, y: r.y, w: r.width, h: r.height }
115
- }
116
- for (const c of children) runFlexLayout(c)
117
- }
118
-
119
- export function forceFlexLayout(hwnd: gui.HWND): void {
120
- const inst = instancesByHwnd.get(hwnd)
121
- if (inst) runFlexLayout(inst)
122
- }
123
-
124
- /**
125
- * 扩展 HostConfig 类型定义
126
- *
127
- * @types/react-reconciler 的 HostConfig 存在以下问题:
128
- * 1. 缺少 rendererPackageName/rendererVersion 属性
129
- *
130
- * 解决方案:扩展补充缺失定义
131
- */
132
- type QuickWinHostConfig = ReactReconciler.HostConfig<
133
- string, Props, Container, Instance, Instance,
134
- never, never, never, gui.HWND,
135
- HostContext, never, number, -1, null
136
- > & {
137
- // rendererPackageName/rendererVersion(原类型定义中缺失)
138
- rendererPackageName: string
139
- rendererVersion: string
140
- }
141
-
142
- let currentUpdatePriority = NoEventPriority
143
-
144
- const hostConfig: QuickWinHostConfig = {
145
- // Core methods
146
- createInstance(type: string, props: Props, rootContainer: Container) {
147
- if (DEBUG) console.log('[reconciler] createInstance called:', type, 'class=' + props.type)
148
- const winClass = props.type
149
- if (isDelayedControl(winClass)) {
150
- if (DEBUG) console.log('[reconciler] delayed control, skipping CreateWindow:', winClass)
151
- return { hwnd: null, type: winClass, props, children: [] }
152
- }
153
- if (winClass === 'STATIC' && props.text !== undefined && props.children !== undefined && props.children !== null) {
154
- throw new Error('[STATIC] text and children are mutually exclusive')
155
- }
156
- const sty = props.style || {}
157
- // reconciler 创建的都是子窗口,确保 WS_CHILD 避免定位异常;创建时不带 WS_VISIBLE 防止 (0,0) 闪烁
158
- const ws = ((props.ws ?? 0) | gui.WindowStyle.CHILD) & ~gui.WindowStyle.VISIBLE
159
- if (DEBUG) console.log('[reconciler] CreateWindow args:', winClass, props.text ?? '', ws, 0, 0, sty.width ?? 100, sty.height ?? 30, rootContainer)
160
- const hwnd = gui.CreateWindow(
161
- winClass, props.text ?? '', ws,
162
- 0, 0,
163
- (typeof sty.width === 'number' ? sty.width : 100) * scaleFactor, (typeof sty.height === 'number' ? sty.height : 30) * scaleFactor,
164
- rootContainer, null
165
- )!
166
- if (dpiFont) gui.SendMessage(hwnd, gui.WmMsg.SETFONT, dpiFont, 1)
167
- if (DEBUG) console.log('[reconciler] createInstance hwnd result:', hwnd, 'null?', hwnd === null)
168
- const instance: Instance = { hwnd, type: winClass, props, children: [] }
169
- setupWindowProc(instance, hwnd)
170
- return instance
171
- },
172
-
173
- createTextInstance(_text: string, _rootContainer: Container) {
174
- throw new Error('should never be called (shouldSetTextContent=true)')
175
- },
176
-
177
- appendInitialChild(parent: Instance, child: Instance) {
178
- if (child.hwnd === null) {
179
- ensureChildWindow(child, parent.hwnd!)
180
- } else {
181
- gui.SetParent(child.hwnd, parent.hwnd)
182
- }
183
- if (DEBUG) console.log('[reconciler] appendInitialChild parent:', parent.hwnd, 'child:', child.hwnd)
184
- parent.children.push(child)
185
- },
186
-
187
- appendChild(parent: Instance, child: Instance) {
188
- if (child.hwnd === null) {
189
- ensureChildWindow(child, parent.hwnd!)
190
- } else {
191
- gui.SetParent(child.hwnd, parent.hwnd)
192
- }
193
- if (DEBUG) console.log('[reconciler] appendChild parent:', parent.hwnd, 'child:', child.hwnd)
194
- parent.children.push(child)
195
- },
196
-
197
- appendChildToContainer(container: Container, child: Instance) {
198
- if (child.hwnd === null) {
199
- ensureChildWindow(child, container)
200
- } else {
201
- gui.SetParent(child.hwnd, container)
202
- }
203
- const rootInst = instancesByHwnd.get(container)
204
- if (rootInst) rootInst.children.push(child)
205
- if (DEBUG) console.log('[reconciler] appendChildToContainer container:', container, 'child hwnd:', child.hwnd)
206
- },
207
-
208
- insertBefore(parent: Instance, child: Instance, beforeChild: Instance) {
209
- if (child.hwnd === null) {
210
- ensureChildWindow(child, parent.hwnd!)
211
- } else {
212
- gui.SetParent(child.hwnd, parent.hwnd)
213
- }
214
- if (DEBUG) console.log('[reconciler] insertBefore parent:', parent.hwnd, 'child:', child.hwnd, 'before:', beforeChild.hwnd)
215
- const idx = parent.children.indexOf(beforeChild)
216
- if (idx >= 0) parent.children.splice(idx, 0, child)
217
- },
218
-
219
- insertInContainerBefore(container: Container, child: Instance, _before: Instance) {
220
- if (child.hwnd === null) {
221
- ensureChildWindow(child, container)
222
- } else {
223
- gui.SetParent(child.hwnd, container)
224
- }
225
- const rootInst = instancesByHwnd.get(container)
226
- if (!rootInst) throw new Error('insertInContainerBefore: no root instance for container')
227
- const idx = rootInst.children.indexOf(_before)
228
- if (idx >= 0) rootInst.children.splice(idx, 0, child)
229
- else rootInst.children.push(child)
230
- if (DEBUG) console.log('[reconciler] insertInContainerBefore container:', container, 'child hwnd:', child.hwnd)
231
- },
232
-
233
- removeChild(parent: Instance, child: Instance) {
234
- if (DEBUG) console.log('[reconciler] removeChild parent:', parent.hwnd, 'child:', child.hwnd, 'type:', child.type)
235
- if (child.hwnd !== null) gui.DestroyWindow(child.hwnd)
236
- const idx = parent.children.indexOf(child)
237
- if (idx >= 0) parent.children.splice(idx, 1)
238
- },
239
-
240
- removeChildFromContainer(container: Container, child: Instance) {
241
- if (child.hwnd === null) return
242
- if (DEBUG) console.log('[reconciler] removeChildFromContainer container:', container, 'child hwnd:', child.hwnd)
243
- gui.DestroyWindow(child.hwnd)
244
- const rootInst = instancesByHwnd.get(container)
245
- if (rootInst) {
246
- const idx = rootInst.children.indexOf(child)
247
- if (idx >= 0) rootInst.children.splice(idx, 1)
248
- }
249
- },
250
-
251
- commitUpdate(instance: Instance, _type: string, oldProps: Props, newProps: Props, _internalHandle: unknown) {
252
- applyProps(instance, newProps, oldProps)
253
- },
254
-
255
- commitMount(_instance: Instance, _type: string, _props: Props, _internal: unknown) { },
256
-
257
- finalizeInitialChildren(instance: Instance, type: string, props: Props) {
258
- if (DEBUG) console.log('[reconciler] finalizeInitialChildren instance:', instance.hwnd, 'props:', JSON.stringify(props))
259
- applyProps(instance, props, { type })
260
- return false
261
- },
262
-
263
- resetAfterCommit(containerInfo: Container) {
264
- if (DEBUG) console.log('[reconciler] resetAfterCommit')
265
- const rootInst = instancesByHwnd.get(containerInfo)
266
- if (!rootInst) throw new Error('resetAfterCommit: no root instance for container')
267
- runFlexLayout(rootInst)
268
- },
269
-
270
- resetTextContent(_instance: Instance) { },
271
-
272
- commitTextUpdate(instance: Instance, _oldText: string, newText: string) {
273
- gui.SetWindowText(instance.hwnd!, newText)
274
- },
275
-
276
- hideInstance(instance: Instance) {
277
- if (instance.hwnd !== null) gui.ShowWindow(instance.hwnd, gui.ShowWindowCmd.HIDE)
278
- },
279
-
280
- hideTextInstance(instance: Instance) {
281
- if (instance.hwnd !== null) gui.ShowWindow(instance.hwnd, gui.ShowWindowCmd.HIDE)
282
- },
283
-
284
- unhideInstance(instance: Instance, _props: Props) {
285
- if (instance.hwnd !== null) gui.ShowWindow(instance.hwnd, gui.ShowWindowCmd.SHOW)
286
- },
287
-
288
- unhideTextInstance(instance: Instance, _text: string) {
289
- if (instance.hwnd !== null) gui.ShowWindow(instance.hwnd, gui.ShowWindowCmd.SHOW)
290
- },
291
-
292
- shouldSetTextContent(_type: string, props: Props) {
293
- return typeof props.children === 'string' || typeof props.children === 'number'
294
- },
295
-
296
- getPublicInstance(instance: Instance) {
297
- return instance.hwnd!
298
- },
299
-
300
- getRootHostContext(_rootContainer: Container) {
301
- return {}
302
- },
303
-
304
- getChildHostContext(_parentContext: HostContext, _type: string) {
305
- return {}
306
- },
307
-
308
- prepareForCommit(_containerInfo: Container) {
309
- return null
310
- },
311
-
312
- preparePortalMount(_containerInfo: Container) { },
313
-
314
- clearContainer(_container: Container) {
315
- // 不需要清理,React reconciler 会通过 removeChildFromContainer 来移除子节点
316
- },
317
-
318
- scheduleTimeout(fn: () => void, delay: number) {
319
- return os.setTimeout(fn, delay)
320
- },
321
-
322
- cancelTimeout(id: os.TimerId) {
323
- os.clearTimeout?.(id)
324
- },
325
-
326
- noTimeout: -1,
327
-
328
- supportsMutation: true,
329
- supportsPersistence: false,
330
- supportsHydration: false,
331
- isPrimaryRenderer: true,
332
- supportsMicrotasks: false,
333
-
334
- setCurrentUpdatePriority(p: number) { currentUpdatePriority = p },
335
- getCurrentUpdatePriority() { return currentUpdatePriority },
336
- resolveUpdatePriority() { return DefaultEventPriority },
337
-
338
- resolveEventType() { return null },
339
- resolveEventTimeStamp() { return Date.now() },
340
- trackSchedulerEvent() { },
341
-
342
- shouldAttemptEagerTransition() { return false },
343
- detachDeletedInstance(instance: Instance) {
344
- if (DEBUG) console.log('[reconciler] detachDeletedInstance hwnd:', instance.hwnd, 'type:', instance.type)
345
- if (instance.hwnd !== null) instancesByHwnd.delete(instance.hwnd)
346
- },
347
-
348
- // 添加必需方法
349
- getInstanceFromNode(_node: unknown) { return null },
350
- beforeActiveInstanceBlur() { },
351
- afterActiveInstanceBlur() { },
352
- prepareScopeUpdate(_scopeInstance: unknown, _instance: unknown) { },
353
- getInstanceFromScope(_scopeInstance: unknown) { return null },
354
-
355
- // Suspense/Concurrent Mode 支持
356
- maySuspendCommit() { return false },
357
- requestPostPaintCallback() { },
358
- preloadInstance() { return true },
359
- startSuspendingCommit() { },
360
- suspendInstance() { },
361
- waitForCommitToBeReady() { return null },
362
-
363
- rendererVersion: '0.1.0',
364
- rendererPackageName: 'react-qw',
365
- NotPendingTransition: null,
366
- // @types/react.Context<T> 与 @types/react-reconciler.ReactContext<T> 是同一运行时对象
367
- // 的两个不兼容类型定义(ReactContext 多 _currentValue 等字段),此 cast 不可避免
368
- HostTransitionContext: createContext(null) as unknown as ReactReconciler.ReactContext<null>,
369
- resetFormInstance(_form: never) { },
370
- }
371
-
372
- const reconciler = ReactReconciler(hostConfig)
373
- export default reconciler
1
+ import ReactReconciler from 'react-reconciler'
2
+ import { createContext } from 'react'
3
+ import { DefaultEventPriority, NoEventPriority } from 'react-reconciler/constants'
4
+ import * as gui from 'gui'
5
+ import * as os from 'os'
6
+ import { applyProps } from './props.js'
7
+ import { calculateFlexLayout, type FlexStyle } from './layout.js'
8
+ import { getButtonIdealSize, measureTextForHwnd } from '../text-measure.js'
9
+ import type { WIntrinsicProps } from './jsx.js'
10
+
11
+ // DEBUG 由 esbuild --define 在 bundle 时替换(见 build.ts),生产环境为 false
12
+ declare const DEBUG: boolean
13
+
14
+ const dpiFont = gui.CreateSystemDpiFont()
15
+ export const scaleFactor = gui.GetScaleFactor()
16
+
17
+ type Container = gui.HWND
18
+ export type Props = WIntrinsicProps
19
+
20
+ export interface Instance {
21
+ hwnd: gui.HWND | null
22
+ type: string
23
+ props: Props
24
+ children: Instance[]
25
+ lastRect?: { x: number; y: number; w: number; h: number }
26
+ }
27
+
28
+ const DELAYED_CONTROLS = new Set(['SysListView32', 'SysTreeView32', 'LISTBOX', 'COMBOBOX', 'msctls_trackbar32', 'SysDateTimePick32', 'SysLink'])
29
+
30
+ function isDelayedControl(winClass: string): boolean {
31
+ return DELAYED_CONTROLS.has(winClass)
32
+ }
33
+
34
+ function setupWindowProc(instance: Instance, hwnd: gui.HWND) {
35
+ instance.hwnd = hwnd
36
+ const oldProc = gui.GetWindowLongPtr(hwnd, gui.Gwlp.WNDPROC)
37
+ instancesByHwnd.set(hwnd, instance)
38
+ gui.SetWindowProc(hwnd, (hwnd: gui.HWND, msg: number, wParam: number, lParam: number) => {
39
+ const ev = instance.props.onEvent
40
+ if (!ev) return gui.CallWindowProc(oldProc, hwnd, msg, wParam, lParam)
41
+ if (typeof ev === 'object') {
42
+ return ev.fn({ hwnd, msg, wParam, lParam, callOldWndProc: () => gui.CallWindowProc(oldProc, hwnd, msg, wParam, lParam) })
43
+ }
44
+ const result = gui.CallWindowProc(oldProc, hwnd, msg, wParam, lParam)
45
+ const override = ev({ hwnd, msg, wParam, lParam })
46
+ return typeof override === 'number' && Number.isInteger(override) ? override : result
47
+ })
48
+ }
49
+
50
+ function ensureChildWindow(child: Instance, parentHwnd: gui.HWND): gui.HWND {
51
+ if (child.hwnd !== null) return child.hwnd
52
+ const sty = child.props.style || {}
53
+ const ws = ((child.props.ws ?? 0) | gui.WindowStyle.CHILD) & ~gui.WindowStyle.VISIBLE
54
+ const hwnd = gui.CreateWindow(
55
+ child.type, child.props.text || '', ws,
56
+ 0, 0,
57
+ (typeof sty.width === 'number' ? sty.width : 100) * scaleFactor, (typeof sty.height === 'number' ? sty.height : 30) * scaleFactor,
58
+ parentHwnd, null
59
+ )!
60
+ if (dpiFont) gui.SendMessage(hwnd, gui.WmMsg.SETFONT, dpiFont, 1)
61
+ setupWindowProc(child, hwnd)
62
+ return hwnd
63
+ }
64
+
65
+ type HostContext = Record<string, never>
66
+
67
+ export const instancesByHwnd = new Map<gui.HWND, Instance>()
68
+
69
+ function runFlexLayout(inst: Instance) {
70
+ const children = inst.children
71
+ if (children.length === 0) return
72
+ const s = inst.props.style
73
+ const flex: FlexStyle = s || {}
74
+ const visible = children.filter(c => !c.props.hidden)
75
+ if (visible.length === 0) return
76
+ const rect = gui.GetClientRect(inst.hwnd!)
77
+ if (!rect) { console.log('flex: no rect for', inst.hwnd, inst.type); return }
78
+ const pw = (rect.right - rect.left) / scaleFactor
79
+ const ph = (rect.bottom - rect.top) / scaleFactor
80
+ if (pw <= 0 || ph <= 0) { console.log('flex: zero size for', inst.hwnd, inst.type, pw, ph); return }
81
+ const autoSizes = new Map<Instance, { w: number; h: number }>()
82
+ for (const child of visible) {
83
+ const sty = child.props.style || {}
84
+ if (!child.hwnd) continue
85
+ if (child.type === 'BUTTON' && (sty.width === 'auto' || sty.height === 'auto')) {
86
+ const ideal = getButtonIdealSize(child.hwnd)
87
+ autoSizes.set(child, { w: ideal.width / scaleFactor, h: ideal.height / scaleFactor })
88
+ } else if (child.type === 'STATIC' && (sty.width === 'auto' || sty.height === 'auto') && child.props.text) {
89
+ const measured = measureTextForHwnd(child.hwnd, child.props.text!)
90
+ autoSizes.set(child, { w: measured.width / scaleFactor, h: measured.height / scaleFactor })
91
+ }
92
+ }
93
+ const results = calculateFlexLayout(flex, pw, ph, visible.map(c => {
94
+ const sty = c.props.style || {}
95
+ const auto = autoSizes.get(c)
96
+ return {
97
+ style: {
98
+ ...sty,
99
+ width: auto ? (sty.width === 'auto' ? auto.w : sty.width) : sty.width,
100
+ height: auto ? (sty.height === 'auto' ? auto.h : sty.height) : sty.height,
101
+ }
102
+ }
103
+ }))
104
+ const pl = flex.paddingLeft ?? flex.padding ?? 0
105
+ const pt = flex.paddingTop ?? flex.padding ?? 0
106
+ for (let i = 0; i < results.length; i++) {
107
+ const r = results[i]!
108
+ const child = visible[i]!
109
+ const lr = child.lastRect
110
+ if (lr && lr.x === r.x && lr.y === r.y && lr.w === r.width && lr.h === r.height) continue
111
+ console.log('flex: set', child.type, child.hwnd, 'to', r.x, r.y, r.width, r.height)
112
+ gui.SetWindowPos(child.hwnd!, gui.SetWindowPosHwnd.TOP, (r.x + pl) * scaleFactor, (r.y + pt) * scaleFactor, r.width * scaleFactor, r.height * scaleFactor, gui.SetWindowPosFlag.SWP_SHOWWINDOW)
113
+ gui.InvalidateRect(child.hwnd!, null, true)
114
+ child.lastRect = { x: r.x, y: r.y, w: r.width, h: r.height }
115
+ }
116
+ for (const c of children) runFlexLayout(c)
117
+ }
118
+
119
+ export function forceFlexLayout(hwnd: gui.HWND): void {
120
+ const inst = instancesByHwnd.get(hwnd)
121
+ if (inst) runFlexLayout(inst)
122
+ }
123
+
124
+ /**
125
+ * 扩展 HostConfig 类型定义
126
+ *
127
+ * @types/react-reconciler 的 HostConfig 存在以下问题:
128
+ * 1. 缺少 rendererPackageName/rendererVersion 属性
129
+ *
130
+ * 解决方案:扩展补充缺失定义
131
+ */
132
+ type QuickWinHostConfig = ReactReconciler.HostConfig<
133
+ string, Props, Container, Instance, Instance,
134
+ never, never, never, gui.HWND,
135
+ HostContext, never, number, -1, null
136
+ > & {
137
+ // rendererPackageName/rendererVersion(原类型定义中缺失)
138
+ rendererPackageName: string
139
+ rendererVersion: string
140
+ }
141
+
142
+ let currentUpdatePriority = NoEventPriority
143
+
144
+ const hostConfig: QuickWinHostConfig = {
145
+ // Core methods
146
+ createInstance(type: string, props: Props, rootContainer: Container) {
147
+ if (DEBUG) console.log('[reconciler] createInstance called:', type, 'class=' + props.type)
148
+ const winClass = props.type
149
+ if (isDelayedControl(winClass)) {
150
+ if (DEBUG) console.log('[reconciler] delayed control, skipping CreateWindow:', winClass)
151
+ return { hwnd: null, type: winClass, props, children: [] }
152
+ }
153
+ if (winClass === 'STATIC' && props.text !== undefined && props.children !== undefined && props.children !== null) {
154
+ throw new Error('[STATIC] text and children are mutually exclusive')
155
+ }
156
+ const sty = props.style || {}
157
+ // reconciler 创建的都是子窗口,确保 WS_CHILD 避免定位异常;创建时不带 WS_VISIBLE 防止 (0,0) 闪烁
158
+ const ws = ((props.ws ?? 0) | gui.WindowStyle.CHILD) & ~gui.WindowStyle.VISIBLE
159
+ if (DEBUG) console.log('[reconciler] CreateWindow args:', winClass, props.text ?? '', ws, 0, 0, sty.width ?? 100, sty.height ?? 30, rootContainer)
160
+ const hwnd = gui.CreateWindow(
161
+ winClass, props.text ?? '', ws,
162
+ 0, 0,
163
+ (typeof sty.width === 'number' ? sty.width : 100) * scaleFactor, (typeof sty.height === 'number' ? sty.height : 30) * scaleFactor,
164
+ rootContainer, null
165
+ )!
166
+ if (dpiFont) gui.SendMessage(hwnd, gui.WmMsg.SETFONT, dpiFont, 1)
167
+ if (DEBUG) console.log('[reconciler] createInstance hwnd result:', hwnd, 'null?', hwnd === null)
168
+ const instance: Instance = { hwnd, type: winClass, props, children: [] }
169
+ setupWindowProc(instance, hwnd)
170
+ return instance
171
+ },
172
+
173
+ createTextInstance(_text: string, _rootContainer: Container) {
174
+ throw new Error('should never be called (shouldSetTextContent=true)')
175
+ },
176
+
177
+ appendInitialChild(parent: Instance, child: Instance) {
178
+ if (child.hwnd === null) {
179
+ ensureChildWindow(child, parent.hwnd!)
180
+ } else {
181
+ gui.SetParent(child.hwnd, parent.hwnd)
182
+ }
183
+ if (DEBUG) console.log('[reconciler] appendInitialChild parent:', parent.hwnd, 'child:', child.hwnd)
184
+ parent.children.push(child)
185
+ },
186
+
187
+ appendChild(parent: Instance, child: Instance) {
188
+ if (child.hwnd === null) {
189
+ ensureChildWindow(child, parent.hwnd!)
190
+ } else {
191
+ gui.SetParent(child.hwnd, parent.hwnd)
192
+ }
193
+ if (DEBUG) console.log('[reconciler] appendChild parent:', parent.hwnd, 'child:', child.hwnd)
194
+ parent.children.push(child)
195
+ },
196
+
197
+ appendChildToContainer(container: Container, child: Instance) {
198
+ if (child.hwnd === null) {
199
+ ensureChildWindow(child, container)
200
+ } else {
201
+ gui.SetParent(child.hwnd, container)
202
+ }
203
+ const rootInst = instancesByHwnd.get(container)
204
+ if (rootInst) rootInst.children.push(child)
205
+ if (DEBUG) console.log('[reconciler] appendChildToContainer container:', container, 'child hwnd:', child.hwnd)
206
+ },
207
+
208
+ insertBefore(parent: Instance, child: Instance, beforeChild: Instance) {
209
+ if (child.hwnd === null) {
210
+ ensureChildWindow(child, parent.hwnd!)
211
+ } else {
212
+ gui.SetParent(child.hwnd, parent.hwnd)
213
+ }
214
+ if (DEBUG) console.log('[reconciler] insertBefore parent:', parent.hwnd, 'child:', child.hwnd, 'before:', beforeChild.hwnd)
215
+ const idx = parent.children.indexOf(beforeChild)
216
+ if (idx >= 0) parent.children.splice(idx, 0, child)
217
+ },
218
+
219
+ insertInContainerBefore(container: Container, child: Instance, _before: Instance) {
220
+ if (child.hwnd === null) {
221
+ ensureChildWindow(child, container)
222
+ } else {
223
+ gui.SetParent(child.hwnd, container)
224
+ }
225
+ const rootInst = instancesByHwnd.get(container)
226
+ if (!rootInst) throw new Error('insertInContainerBefore: no root instance for container')
227
+ const idx = rootInst.children.indexOf(_before)
228
+ if (idx >= 0) rootInst.children.splice(idx, 0, child)
229
+ else rootInst.children.push(child)
230
+ if (DEBUG) console.log('[reconciler] insertInContainerBefore container:', container, 'child hwnd:', child.hwnd)
231
+ },
232
+
233
+ removeChild(parent: Instance, child: Instance) {
234
+ if (DEBUG) console.log('[reconciler] removeChild parent:', parent.hwnd, 'child:', child.hwnd, 'type:', child.type)
235
+ if (child.hwnd !== null) gui.DestroyWindow(child.hwnd)
236
+ const idx = parent.children.indexOf(child)
237
+ if (idx >= 0) parent.children.splice(idx, 1)
238
+ },
239
+
240
+ removeChildFromContainer(container: Container, child: Instance) {
241
+ if (child.hwnd === null) return
242
+ if (DEBUG) console.log('[reconciler] removeChildFromContainer container:', container, 'child hwnd:', child.hwnd)
243
+ gui.DestroyWindow(child.hwnd)
244
+ const rootInst = instancesByHwnd.get(container)
245
+ if (rootInst) {
246
+ const idx = rootInst.children.indexOf(child)
247
+ if (idx >= 0) rootInst.children.splice(idx, 1)
248
+ }
249
+ },
250
+
251
+ commitUpdate(instance: Instance, _type: string, oldProps: Props, newProps: Props, _internalHandle: unknown) {
252
+ applyProps(instance, newProps, oldProps)
253
+ },
254
+
255
+ commitMount(_instance: Instance, _type: string, _props: Props, _internal: unknown) { },
256
+
257
+ finalizeInitialChildren(instance: Instance, type: string, props: Props) {
258
+ if (DEBUG) console.log('[reconciler] finalizeInitialChildren instance:', instance.hwnd, 'props:', JSON.stringify(props))
259
+ applyProps(instance, props, { type })
260
+ return false
261
+ },
262
+
263
+ resetAfterCommit(containerInfo: Container) {
264
+ if (DEBUG) console.log('[reconciler] resetAfterCommit')
265
+ const rootInst = instancesByHwnd.get(containerInfo)
266
+ if (!rootInst) throw new Error('resetAfterCommit: no root instance for container')
267
+ runFlexLayout(rootInst)
268
+ },
269
+
270
+ resetTextContent(_instance: Instance) { },
271
+
272
+ commitTextUpdate(instance: Instance, _oldText: string, newText: string) {
273
+ gui.SetWindowText(instance.hwnd!, newText)
274
+ },
275
+
276
+ hideInstance(instance: Instance) {
277
+ if (instance.hwnd !== null) gui.ShowWindow(instance.hwnd, gui.ShowWindowCmd.HIDE)
278
+ },
279
+
280
+ hideTextInstance(instance: Instance) {
281
+ if (instance.hwnd !== null) gui.ShowWindow(instance.hwnd, gui.ShowWindowCmd.HIDE)
282
+ },
283
+
284
+ unhideInstance(instance: Instance, _props: Props) {
285
+ if (instance.hwnd !== null) gui.ShowWindow(instance.hwnd, gui.ShowWindowCmd.SHOW)
286
+ },
287
+
288
+ unhideTextInstance(instance: Instance, _text: string) {
289
+ if (instance.hwnd !== null) gui.ShowWindow(instance.hwnd, gui.ShowWindowCmd.SHOW)
290
+ },
291
+
292
+ shouldSetTextContent(_type: string, props: Props) {
293
+ return typeof props.children === 'string' || typeof props.children === 'number'
294
+ },
295
+
296
+ getPublicInstance(instance: Instance) {
297
+ return instance.hwnd!
298
+ },
299
+
300
+ getRootHostContext(_rootContainer: Container) {
301
+ return {}
302
+ },
303
+
304
+ getChildHostContext(_parentContext: HostContext, _type: string) {
305
+ return {}
306
+ },
307
+
308
+ prepareForCommit(_containerInfo: Container) {
309
+ return null
310
+ },
311
+
312
+ preparePortalMount(_containerInfo: Container) { },
313
+
314
+ clearContainer(_container: Container) {
315
+ // 不需要清理,React reconciler 会通过 removeChildFromContainer 来移除子节点
316
+ },
317
+
318
+ scheduleTimeout(fn: () => void, delay: number) {
319
+ return os.setTimeout(fn, delay)
320
+ },
321
+
322
+ cancelTimeout(id: os.TimerId) {
323
+ os.clearTimeout?.(id)
324
+ },
325
+
326
+ noTimeout: -1,
327
+
328
+ supportsMutation: true,
329
+ supportsPersistence: false,
330
+ supportsHydration: false,
331
+ isPrimaryRenderer: true,
332
+ supportsMicrotasks: false,
333
+
334
+ setCurrentUpdatePriority(p: number) { currentUpdatePriority = p },
335
+ getCurrentUpdatePriority() { return currentUpdatePriority },
336
+ resolveUpdatePriority() { return DefaultEventPriority },
337
+
338
+ resolveEventType() { return null },
339
+ resolveEventTimeStamp() { return Date.now() },
340
+ trackSchedulerEvent() { },
341
+
342
+ shouldAttemptEagerTransition() { return false },
343
+ detachDeletedInstance(instance: Instance) {
344
+ if (DEBUG) console.log('[reconciler] detachDeletedInstance hwnd:', instance.hwnd, 'type:', instance.type)
345
+ if (instance.hwnd !== null) instancesByHwnd.delete(instance.hwnd)
346
+ },
347
+
348
+ // 添加必需方法
349
+ getInstanceFromNode(_node: unknown) { return null },
350
+ beforeActiveInstanceBlur() { },
351
+ afterActiveInstanceBlur() { },
352
+ prepareScopeUpdate(_scopeInstance: unknown, _instance: unknown) { },
353
+ getInstanceFromScope(_scopeInstance: unknown) { return null },
354
+
355
+ // Suspense/Concurrent Mode 支持
356
+ maySuspendCommit() { return false },
357
+ requestPostPaintCallback() { },
358
+ preloadInstance() { return true },
359
+ startSuspendingCommit() { },
360
+ suspendInstance() { },
361
+ waitForCommitToBeReady() { return null },
362
+
363
+ rendererVersion: '0.1.0',
364
+ rendererPackageName: 'react-qw',
365
+ NotPendingTransition: null,
366
+ // @types/react.Context<T> 与 @types/react-reconciler.ReactContext<T> 是同一运行时对象
367
+ // 的两个不兼容类型定义(ReactContext 多 _currentValue 等字段),此 cast 不可避免
368
+ HostTransitionContext: createContext(null) as unknown as ReactReconciler.ReactContext<null>,
369
+ resetFormInstance(_form: never) { },
370
+ }
371
+
372
+ const reconciler = ReactReconciler(hostConfig)
373
+ export default reconciler