quickwin 1.3.0 → 2.0.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 (143) hide show
  1. package/README.en.md +1 -2
  2. package/README.md +19 -23
  3. package/examples/PdfCanvas.js +8 -37
  4. package/examples/PdfCanvas.tsx +11 -41
  5. package/examples/exec_server.js +20054 -1423
  6. package/examples/exec_server.tsx +489 -0
  7. package/examples/exec_server_worker.js +383 -33
  8. package/examples/exec_server_worker.ts +408 -30
  9. package/examples/pdf_preview2.js +25 -82
  10. package/examples/pdf_preview2.ts +27 -84
  11. package/examples/pdf_viewer.js +1318 -1178
  12. package/examples/pdf_viewer.tsx +16 -67
  13. package/examples/setres.js +22 -47
  14. package/examples/setres.ts +23 -57
  15. package/examples/test_gallery.js +1313 -1074
  16. package/examples/test_react_lazy.js +1284 -1056
  17. package/examples/test_react_listbox.js +1283 -1055
  18. package/examples/test_root_window.js +1279 -1051
  19. package/examples/test_tab.js +1284 -1056
  20. package/lib/abort.js +70 -0
  21. package/lib/abort.ts +69 -0
  22. package/lib/certs.js +214 -0
  23. package/lib/certs.ts +224 -0
  24. package/lib/exec-log.js +15 -0
  25. package/lib/exec-log.ts +30 -0
  26. package/lib/fetch.js +37 -4
  27. package/lib/fetch.ts +35 -4
  28. package/lib/ffi/bind.js +352 -0
  29. package/lib/ffi/bind.ts +442 -0
  30. package/lib/ffi/ctype.js +168 -0
  31. package/lib/ffi/ctype.ts +331 -0
  32. package/lib/ffi/index.js +6 -0
  33. package/lib/ffi/index.ts +6 -0
  34. package/lib/ffi/struct.js +412 -0
  35. package/lib/ffi/struct.ts +664 -0
  36. package/lib/http-common.js +43 -5
  37. package/lib/http-common.ts +48 -6
  38. package/lib/http-server.js +187 -29
  39. package/lib/http-server.ts +177 -26
  40. package/lib/react-qw/components/DateTimePicker.js +18 -37
  41. package/lib/react-qw/components/Input.js +3 -10
  42. package/lib/react-qw/components/Link.js +6 -19
  43. package/lib/react-qw/components/ListView.js +49 -154
  44. package/lib/react-qw/components/PathPicker.js +28 -88
  45. package/lib/react-qw/components/ScrollView.js +15 -8
  46. package/lib/react-qw/components/Slider.js +2 -1
  47. package/lib/react-qw/components/Tab.js +6 -22
  48. package/lib/react-qw/components/Tooltip.js +10 -21
  49. package/lib/react-qw/components/TreeView.js +8 -31
  50. package/lib/react-qw/index.js +4 -2
  51. package/lib/react-qw/index.ts +4 -2
  52. package/lib/react-qw/nmhdr.js +3 -24
  53. package/lib/react-qw/nmhdr.ts +5 -30
  54. package/lib/react-qw/props.js +7 -2
  55. package/lib/react-qw/props.ts +7 -3
  56. package/lib/react-qw/reconciler.js +23 -13
  57. package/lib/react-qw/reconciler.ts +22 -13
  58. package/lib/text-measure.js +18 -23
  59. package/lib/text-measure.ts +20 -25
  60. package/lib/websocket.js +12 -2
  61. package/lib/websocket.ts +15 -2
  62. package/lib/windows/comctl32.js +18 -0
  63. package/lib/windows/comctl32.ts +20 -0
  64. package/lib/windows/comdlg32.js +7 -0
  65. package/lib/windows/comdlg32.ts +9 -0
  66. package/lib/windows/gdi32.js +90 -0
  67. package/lib/windows/gdi32.ts +97 -0
  68. package/lib/windows/kernel32.js +66 -0
  69. package/lib/windows/kernel32.ts +76 -0
  70. package/lib/windows/ntdll.js +8 -0
  71. package/lib/windows/ntdll.ts +10 -0
  72. package/lib/windows/ole32.js +7 -0
  73. package/lib/windows/ole32.ts +9 -0
  74. package/lib/windows/shell32.js +11 -0
  75. package/lib/windows/shell32.ts +14 -0
  76. package/lib/windows/structs.js +384 -0
  77. package/lib/windows/structs.ts +422 -0
  78. package/lib/windows/user32.js +469 -0
  79. package/lib/windows/user32.ts +502 -0
  80. package/package.json +1 -1
  81. package/quickwin.d.ts +22 -37
  82. package/quickwin_const.d.ts +1 -0
  83. package/qwin-nowasm-x86.exe +0 -0
  84. package/qwin-nowasm.exe +0 -0
  85. package/qwin-x86.exe +0 -0
  86. package/qwin.exe +0 -0
  87. package/test/bench_ffi_callraw.js +236 -0
  88. package/test/bench_ffi_callraw.ts +236 -0
  89. package/test/bench_ffi_overhead.js +139 -0
  90. package/test/bench_ffi_overhead.ts +141 -0
  91. package/test/bench_sendmessage.js +104 -0
  92. package/test/bench_sendmessage.ts +100 -0
  93. package/test/bench_sendmessage_nonnull.js +187 -0
  94. package/test/bench_sendmessage_nonnull.ts +188 -0
  95. package/test/run.js +12 -4
  96. package/test/run.ts +13 -5
  97. package/test/test_basic_fetch.js +8 -0
  98. package/test/test_basic_fetch.ts +9 -0
  99. package/test/test_ffi.js +25 -43
  100. package/test/test_ffi.ts +33 -52
  101. package/test/test_ffi_abi.js +235 -0
  102. package/test/test_ffi_abi.ts +218 -0
  103. package/test/test_ffi_bind.js +538 -8
  104. package/test/test_ffi_bind.ts +559 -8
  105. package/test/test_ffi_struct.js +632 -88
  106. package/test/test_ffi_struct.ts +666 -91
  107. package/test/test_ffi_struct_layout.js +495 -0
  108. package/test/test_ffi_struct_layout.ts +543 -0
  109. package/test/test_ffi_user32.js +103 -0
  110. package/test/test_ffi_user32.ts +113 -0
  111. package/test/test_ffi_windows.js +101 -0
  112. package/test/test_ffi_windows.ts +113 -0
  113. package/test/test_helper.js +8 -1
  114. package/test/test_helper.ts +9 -1
  115. package/test/test_http_import.js +18 -0
  116. package/test/test_http_import.ts +19 -0
  117. package/test/test_http_server.js +73 -1
  118. package/test/test_http_server.ts +75 -1
  119. package/test/test_ipv6.js +2 -2
  120. package/test/test_ipv6.ts +2 -2
  121. package/test/test_net_fetch.js +70 -8
  122. package/test/test_net_fetch.ts +69 -7
  123. package/test/test_net_websocket.js +1 -1
  124. package/test/test_net_websocket.ts +1 -1
  125. package/test/test_react_complex.js +1294 -1066
  126. package/test/test_react_counter.js +1284 -1056
  127. package/test/test_react_flex.js +1304 -1069
  128. package/test/test_react_hidden.js +1287 -1059
  129. package/test/test_react_listview_link.js +1284 -1056
  130. package/test/test_react_notify.js +18534 -0
  131. package/test/test_react_render.js +1284 -1056
  132. package/test/test_wolfssl_read.js +3 -2
  133. package/test/test_wolfssl_read.ts +3 -1
  134. package/test/test_worker_net.js +35 -17
  135. package/test/test_worker_net.ts +27 -15
  136. package/tsconfig.json +1 -1
  137. package/examples/exec_server.ts +0 -80
  138. package/lib/ffi-bind.js +0 -85
  139. package/lib/ffi-bind.ts +0 -170
  140. package/lib/ffi-struct.js +0 -224
  141. package/lib/ffi-struct.ts +0 -239
  142. package/lib/win-common-structs.js +0 -39
  143. package/lib/win-common-structs.ts +0 -41
@@ -1,6 +1,7 @@
1
1
  import './polyfill.js'
2
2
  import * as sock from 'sock'
3
3
  import { HeadersImpl, RequestImpl, ResponseImpl, _readStream } from './http-common.js'
4
+ import { AbortController } from './abort.js'
4
5
 
5
6
  type SockHandle = import('sock').SockHandle
6
7
 
@@ -36,10 +37,14 @@ interface Conn {
36
37
  pending: PendingReq | null
37
38
  current: ActiveReq | null
38
39
  outQueue: Uint8Array | null
40
+ resumeRead: (() => void) | null
39
41
  closed: boolean
40
42
  closeAfterFlush: boolean
41
43
  keepAlive: boolean
42
44
  owner: HttpServer
45
+ /** AbortController for the request currently served on this connection,
46
+ * aborted when the client disconnects mid-response. */
47
+ ac: AbortController | null
43
48
  }
44
49
 
45
50
  const STATUS_TEXT: Record<number, string> = {
@@ -74,6 +79,20 @@ function findCrLfCrLf(buf: Uint8Array): number {
74
79
  return -1
75
80
  }
76
81
 
82
+ /** Flatten a `HeadersInit` into `[name, value]` pairs, preserving the
83
+ * casing the caller provided (Node `addTrailers` semantics). */
84
+ function headersInitToPairs(init: HeadersInit): [string, string][] {
85
+ if (init instanceof HeadersImpl) {
86
+ const out: [string, string][] = []
87
+ for (const [name, value] of init) out.push([name, value])
88
+ return out
89
+ }
90
+ if (Array.isArray(init)) return init.map(([name, value]) => [name, String(value)])
91
+ const out: [string, string][] = []
92
+ for (const name of Object.keys(init)) out.push([name, String((init as Record<string, string>)[name])])
93
+ return out
94
+ }
95
+
77
96
  /** Decode a chunked-transfer body starting at `start`. Returns the decoded
78
97
  * data and the offset just past the terminating CRLF, or null if the buffer
79
98
  * does not yet contain the complete chunked body. */
@@ -196,10 +215,12 @@ export class HttpServer {
196
215
  pending: null,
197
216
  current: null,
198
217
  outQueue: null,
218
+ resumeRead: null,
199
219
  closed: false,
200
220
  closeAfterFlush: false,
201
221
  keepAlive: true,
202
222
  owner: this,
223
+ ac: null,
203
224
  }
204
225
  this.conns.set(c.sock, c)
205
226
  sock.set_on_event(c.sock, (event: NetEvent) => this.onConnEvent(c, event))
@@ -212,7 +233,8 @@ export class HttpServer {
212
233
  if (event.lNetworkEvents & sock.FdEvent.FD_WRITE) flushQueue(c)
213
234
  if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
214
235
  onRead(c)
215
- if (!c.current && !c.pending) closeConn(c)
236
+ abortConn(c)
237
+ closeConn(c)
216
238
  }
217
239
  }
218
240
  }
@@ -233,48 +255,137 @@ function onRead(c: Conn): void {
233
255
  if (!c.closed) tryProcess(c)
234
256
  }
235
257
 
236
- function queueSend(c: Conn, data: Uint8Array): void {
237
- if (c.closed || data.length === 0) return
258
+ /** Send `data`, or queue it when the kernel send buffer is full.
259
+ * Returns true when the data was fully handed to the kernel, or false
260
+ * when it has been buffered (or partially handed over) and the caller
261
+ * should back off until `flushQueue` drains and calls `c.resumeRead`. */
262
+ function queueSend(c: Conn, data: Uint8Array): boolean {
263
+ if (c.closed || data.length === 0) return true
238
264
  if (c.outQueue && c.outQueue.length > 0) {
239
265
  c.outQueue = concatU8([c.outQueue, data])
240
- return
266
+ return false
241
267
  }
242
268
  const ret = sock.send(c.sock, toArrayBuffer(data))
243
269
  if (ret < 0) {
244
- c.outQueue = data
245
- return
270
+ abortConn(c)
271
+ closeConn(c)
272
+ return true
246
273
  }
247
274
  if (ret < data.length) {
248
275
  c.outQueue = data.subarray(ret)
249
- return
276
+ return false
250
277
  }
251
- if (c.closeAfterFlush) closeConn(c)
278
+ return true
252
279
  }
253
280
 
254
281
  function flushQueue(c: Conn): void {
255
282
  if (c.closed) return
256
283
  if (!c.outQueue || c.outQueue.length === 0) {
257
284
  if (c.closeAfterFlush) closeConn(c)
285
+ else resumeReader(c)
258
286
  return
259
287
  }
260
288
  const q = c.outQueue
261
289
  const ret = sock.send(c.sock, toArrayBuffer(q))
262
- if (ret < 0) return
290
+ if (ret < 0) {
291
+ abortConn(c)
292
+ closeConn(c)
293
+ return
294
+ }
263
295
  if (ret < q.length) {
264
296
  c.outQueue = q.subarray(ret)
265
297
  return
266
298
  }
267
299
  c.outQueue = null
268
300
  if (c.closeAfterFlush) closeConn(c)
301
+ else resumeReader(c)
269
302
  }
270
303
 
271
304
  function closeConn(c: Conn): void {
272
305
  if (c.closed) return
273
306
  c.closed = true
307
+ resumeReader(c)
274
308
  if (c.sock >= 0) sock.closesocket(c.sock)
275
309
  c.owner.conns.delete(c.sock)
276
310
  }
277
311
 
312
+ /** Abort the in-flight request (notifying its handler via `req.signal`).
313
+ * Safe to call when idle or already aborted: the controller is cleared so
314
+ * a fresh request on this connection gets a fresh signal. */
315
+ function abortConn(c: Conn): void {
316
+ const ac = c.ac
317
+ c.ac = null
318
+ if (ac) ac.abort()
319
+ }
320
+
321
+ /** Resolve the pending back-pressure awaiter, if any. Called whenever the
322
+ * send queue drains or the connection closes. */
323
+ function resumeReader(c: Conn): void {
324
+ const r = c.resumeRead
325
+ c.resumeRead = null
326
+ if (r) r()
327
+ }
328
+
329
+ /** Close the connection once a response is fully flushed, when the
330
+ * handler or client asked for `Connection: close`. */
331
+ function finishResponse(c: Conn): void {
332
+ if (c.closeAfterFlush && !c.outQueue) closeConn(c)
333
+ }
334
+
335
+ /** Frame `data` as a single chunked-transfer block: `<hex size>\r\n<data>\r\n`. */
336
+ function encodeChunkFrame(data: Uint8Array, enc: TextEncoder): Uint8Array {
337
+ return concatU8([enc.encode(data.byteLength.toString(16) + '\r\n'), data, enc.encode('\r\n')])
338
+ }
339
+
340
+ /** Read `body` one chunk at a time and send it out. When the kernel send
341
+ * buffer fills up (`queueSend` returns false) we pause on `c.resumeRead`,
342
+ * which `flushQueue` resumes once the socket drains — backpressure. */
343
+ async function writeStreamingBody(c: Conn, body: ReadableStream<Uint8Array> | null, chunked: boolean, trailer: Promise<HeadersInit> | undefined): Promise<void> {
344
+ if (c.closed || body == null) return
345
+ const reader = body.getReader()
346
+ const enc = new TextEncoder()
347
+ try {
348
+ while (!c.closed) {
349
+ const { done, value } = await reader.read()
350
+ if (done) break
351
+ if (value && value.byteLength > 0) {
352
+ const frame = chunked ? encodeChunkFrame(value, enc) : value
353
+ if (!queueSend(c, frame)) {
354
+ await new Promise<void>((resolve) => { c.resumeRead = resolve })
355
+ }
356
+ }
357
+ }
358
+ if (c.closed) {
359
+ // 客户端断开(FD_CLOSE / send 失败):取消响应体,让 producer 感知
360
+ //(exec_server 借此 kill 子进程),而不是干等它自然结束。
361
+ try { await reader.cancel() } catch (e) {
362
+ console.error('[http-server] body cancel error:', e)
363
+ }
364
+ return
365
+ }
366
+ if (chunked) {
367
+ let terminator = '0\r\n\r\n'
368
+ if (trailer) {
369
+ try {
370
+ const pairs = headersInitToPairs(await trailer)
371
+ if (pairs.length > 0) {
372
+ let s = '0\r\n'
373
+ for (const [name, value] of pairs) s += name + ': ' + value + '\r\n'
374
+ s += '\r\n'
375
+ terminator = s
376
+ }
377
+ } catch (e) {
378
+ console.error('[http-server] trailers promise rejected:', e)
379
+ }
380
+ }
381
+ queueSend(c, enc.encode(terminator))
382
+ }
383
+ finishResponse(c)
384
+ } finally {
385
+ reader.releaseLock()
386
+ }
387
+ }
388
+
278
389
  // ── Request state machine ──
279
390
 
280
391
  function whole(c: Conn): Uint8Array {
@@ -355,12 +466,15 @@ function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: n
355
466
  const isAbsolute = parsed.target.startsWith('http://') || parsed.target.startsWith('https://')
356
467
  const url = isAbsolute ? parsed.target : 'http://' + host + parsed.target
357
468
 
469
+ const ac = new AbortController()
358
470
  const req = new RequestImpl(url, {
359
471
  method: parsed.method,
360
472
  headers: new HeadersImpl(parsed.headers),
361
473
  body: body.length > 0 ? body : undefined,
474
+ signal: ac.signal,
362
475
  })
363
476
 
477
+ c.ac = ac
364
478
  c.current = { req }
365
479
  trimParts(c, whole(c), end)
366
480
  void handleRequest(c)
@@ -385,40 +499,77 @@ async function handleRequest(c: Conn): Promise<void> {
385
499
  }
386
500
 
387
501
  if (c.closed) return
388
- await sendResponse(c, req.method, res)
502
+ try {
503
+ await sendResponse(c, req.method, res)
504
+ } catch (e) {
505
+ console.error('[http-server] response send error:', e)
506
+ if (!c.closed) closeConn(c)
507
+ return
508
+ }
389
509
  if (c.closed) return
390
510
 
391
511
  c.current = null
392
512
  if (!c.closeAfterFlush) tryProcess(c)
393
513
  }
394
514
 
515
+ function buildHead(status: number, statusText: string, headers: HeadersImpl): Uint8Array {
516
+ let head = 'HTTP/1.1 ' + status + ' ' + statusText + '\r\n'
517
+ for (const [name, value] of headers) head += name + ': ' + value + '\r\n'
518
+ head += '\r\n'
519
+ return new TextEncoder().encode(head)
520
+ }
521
+
395
522
  async function sendResponse(c: Conn, method: string, res: Response): Promise<void> {
396
523
  const status = res.status
397
524
  const statusText = res.statusText || STATUS_TEXT[status] || 'OK'
398
525
  const headers = new HeadersImpl(res.headers)
399
526
 
400
- let body: Uint8Array
401
- try {
402
- body = await _readStream(res.body)
403
- } catch {
404
- body = new Uint8Array(0)
405
- }
406
- if (c.closed) return
407
-
408
527
  const headOnly = method === 'HEAD'
409
528
  const bodyless = (status >= 100 && status < 200) || status === 204 || status === 304
410
529
 
411
- if (!headers.has('content-length') && !bodyless) headers.set('Content-Length', String(body.length))
412
530
  if (!headers.has('connection')) headers.set('Connection', c.keepAlive ? 'keep-alive' : 'close')
413
-
414
531
  const connHdr = (headers.get('connection') || '').toLowerCase()
415
532
  if (!c.keepAlive || connHdr === 'close') c.closeAfterFlush = true
416
533
 
417
- let head = 'HTTP/1.1 ' + status + ' ' + statusText + '\r\n'
418
- for (const [name, value] of headers) head += name + ': ' + value + '\r\n'
419
- head += '\r\n'
534
+ if (bodyless) {
535
+ headers.delete('Transfer-Encoding')
536
+ headers.delete('Content-Length')
537
+ headers.delete('Trailer')
538
+ queueSend(c, buildHead(status, statusText, headers))
539
+ finishResponse(c)
540
+ return
541
+ }
420
542
 
421
- const headBytes = new TextEncoder().encode(head)
422
- const payload = headOnly || body.length === 0 ? headBytes : concatU8([headBytes, body])
423
- queueSend(c, payload)
543
+ if (headOnly) {
544
+ headers.delete('Transfer-Encoding')
545
+ headers.delete('Trailer')
546
+ if (!headers.has('content-length')) {
547
+ try {
548
+ const full = await _readStream(res.body)
549
+ headers.set('Content-Length', String(full.length))
550
+ } catch { /* leave length unspecified */ }
551
+ }
552
+ if (c.closed) return
553
+ queueSend(c, buildHead(status, statusText, headers))
554
+ finishResponse(c)
555
+ return
556
+ }
557
+
558
+ // Body is streamed out chunk by chunk. A declared Content-Length sends
559
+ // the bytes as-is; otherwise we use chunked framing.
560
+ const chunked = (headers.get('transfer-encoding') || '').toLowerCase() === 'chunked' ||
561
+ !headers.has('content-length')
562
+ // Trailing headers are only emitted when the body is chunk-framed and a
563
+ // `Trailer` declaration header is present (Node addTrailers semantics).
564
+ const trailerDeclared = (headers.get('trailer') || '').trim().length > 0
565
+ if (chunked) {
566
+ headers.delete('Content-Length')
567
+ if (!headers.has('transfer-encoding')) headers.set('Transfer-Encoding', 'chunked')
568
+ if (!trailerDeclared) headers.delete('Trailer')
569
+ } else {
570
+ headers.delete('Transfer-Encoding')
571
+ headers.delete('Trailer')
572
+ }
573
+ queueSend(c, buildHead(status, statusText, headers))
574
+ await writeStreamingBody(c, res.body, chunked, (chunked && trailerDeclared) ? res.trailers : undefined)
424
575
  }
@@ -1,33 +1,21 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, useRef, useEffect, useState } from 'react';
3
3
  import * as gui from 'gui';
4
- import * as ffi from 'ffi';
5
- import { struct } from '../../ffi-struct.js';
6
- import { NMHDR, nmCode, readI32At, readU16At } from '../nmhdr.js';
7
- const SYSTEMTIME = struct({
8
- wYear: 'u16', wMonth: 'u16', wDayOfWeek: 'u16', wDay: 'u16',
9
- wHour: 'u16', wMinute: 'u16', wSecond: 'u16', wMilliseconds: 'u16',
10
- });
11
- const NMDATETIMECHANGE = struct({
12
- hdr: NMHDR,
13
- dwFlags: 'u32',
14
- st: SYSTEMTIME,
15
- });
16
- function bufPtr(buf) {
17
- return ffi.bufferPtr(buf);
18
- }
4
+ import { InvalidateRect } from '../../windows/user32.js';
5
+ import { NULL } from '../../ffi/ctype.js';
6
+ import { SYSTEMTIME, NMDATETIMECHANGE } from '../../windows/structs.js';
7
+ import { nmCode } from '../nmhdr.js';
19
8
  function dateToSysTimeBuf(d) {
20
- const buf = new ArrayBuffer(16);
21
- const dv = new DataView(buf);
22
- dv.setUint16(0, d.getFullYear(), true);
23
- dv.setUint16(2, d.getMonth() + 1, true);
24
- dv.setUint16(4, 0, true); // wDayOfWeek (ignored on set)
25
- dv.setUint16(6, d.getDate(), true);
26
- dv.setUint16(8, d.getHours(), true);
27
- dv.setUint16(10, d.getMinutes(), true);
28
- dv.setUint16(12, d.getSeconds(), true);
29
- dv.setUint16(14, d.getMilliseconds(), true);
30
- return buf;
9
+ // wDayOfWeek 设置时被忽略,缺省跳过 = 0
10
+ return SYSTEMTIME.encode({
11
+ wYear: d.getFullYear(),
12
+ wMonth: d.getMonth() + 1,
13
+ wDay: d.getDate(),
14
+ wHour: d.getHours(),
15
+ wMinute: d.getMinutes(),
16
+ wSecond: d.getSeconds(),
17
+ wMilliseconds: d.getMilliseconds(),
18
+ });
31
19
  }
32
20
  const DateTimePicker = forwardRef(function DateTimePicker({ value, onChange, defaultValue, format = 'short', allowNone, updown, style }, ref) {
33
21
  const [internalDate, setInternalDate] = useState(defaultValue ?? null);
@@ -52,7 +40,7 @@ const DateTimePicker = forwardRef(function DateTimePicker({ value, onChange, def
52
40
  const d = effectiveDate;
53
41
  if (d) {
54
42
  const buf = dateToSysTimeBuf(d);
55
- gui.SendMessage(h, 4098 /* gui.DtMsg.SETSYSTEMTIME */, 0 /* gui.DtFlag.GDT_VALID */, bufPtr(buf));
43
+ gui.SendMessage(h, 4098 /* gui.DtMsg.SETSYSTEMTIME */, 0 /* gui.DtFlag.GDT_VALID */, buf.ptr);
56
44
  }
57
45
  else {
58
46
  gui.SendMessage(h, 4098 /* gui.DtMsg.SETSYSTEMTIME */, 1 /* gui.DtFlag.GDT_NONE */, 0);
@@ -62,7 +50,7 @@ const DateTimePicker = forwardRef(function DateTimePicker({ value, onChange, def
62
50
  const h = dpRef.current;
63
51
  if (!h)
64
52
  return;
65
- gui.InvalidateRect(h, null, true);
53
+ InvalidateRect(h, NULL, 1);
66
54
  // 发送 WM_SIZE 让控件(尤其是 DTS_UPDOWN)重新布局内部子窗口
67
55
  const cr = gui.GetClientRect(h);
68
56
  if (cr) {
@@ -76,21 +64,14 @@ const DateTimePicker = forwardRef(function DateTimePicker({ value, onChange, def
76
64
  const code = nmCode(e.lParam);
77
65
  if (code === -759 /* gui.DtNotifyCode.DATETIMECHANGE */) {
78
66
  // NMDATETIMECHANGE: NMHDR + DWORD dwFlags + SYSTEMTIME st
79
- const st = NMDATETIMECHANGE.offsetOf('st');
80
- const dwFlags = readI32At(e.lParam, NMDATETIMECHANGE.offsetOf('dwFlags'));
67
+ const { dwFlags, st } = NMDATETIMECHANGE.decode(e.lParam);
81
68
  if (dwFlags === 1 /* gui.DtFlag.GDT_NONE */) {
82
69
  if (!isControlled)
83
70
  setInternalDate(null);
84
71
  onChangeRef.current?.(null);
85
72
  }
86
73
  else {
87
- const year = readU16At(e.lParam, st + 0);
88
- const month = readU16At(e.lParam, st + 2);
89
- const day = readU16At(e.lParam, st + 6);
90
- const hour = readU16At(e.lParam, st + 8);
91
- const min = readU16At(e.lParam, st + 10);
92
- const sec = readU16At(e.lParam, st + 12);
93
- const d = new Date(year, month - 1, day, hour, min, sec);
74
+ const d = new Date(st.wYear, st.wMonth - 1, st.wDay, st.wHour, st.wMinute, st.wSecond);
94
75
  if (!isControlled)
95
76
  setInternalDate(d);
96
77
  onChangeRef.current?.(d);
@@ -1,14 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, useState, useRef, useEffect } from 'react';
3
3
  import * as gui from 'gui';
4
- import * as ffi from 'ffi';
5
- function textToUtf16(s) {
6
- const buf = new ArrayBuffer((s.length + 1) * 2);
7
- const dv = new DataView(buf);
8
- for (let i = 0; i < s.length; i++)
9
- dv.setUint16(i * 2, s.charCodeAt(i), true);
10
- return buf;
11
- }
4
+ import { WCHAR } from '../../ffi/bind.js';
12
5
  export const Input = forwardRef(({ value: controlledValue, defaultValue = '', onChange, placeholder, password, multiline, readonly, number, disabled, style }, ref) => {
13
6
  const [internalValue, setInternalValue] = useState(defaultValue);
14
7
  const isControlled = controlledValue !== undefined;
@@ -27,8 +20,8 @@ export const Input = forwardRef(({ value: controlledValue, defaultValue = '', on
27
20
  const h = inputRef.current;
28
21
  if (!h || !placeholder)
29
22
  return;
30
- const buf = textToUtf16(placeholder);
31
- gui.SendMessage(h, 5377 /* gui.EditMsg.SETCUEBANNER */, 1, ffi.bufferPtr(buf));
23
+ const buf = WCHAR.encode(placeholder);
24
+ gui.SendMessage(h, 5377 /* gui.EditMsg.SETCUEBANNER */, 1, buf.ptr);
32
25
  }, [placeholder]);
33
26
  return (_jsx("w", { type: "EDIT", text: displayValue, ws: ws, style: style, disabled: disabled, ref: (h) => {
34
27
  inputRef.current = h;
@@ -1,20 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, useRef, useEffect } from 'react';
3
- import * as gui from 'gui';
4
- import * as ffi from 'ffi';
5
- import { nmCode, NMHDR_SIZE } from '../nmhdr.js';
6
- function readUtf16(ptr, offset, maxWords) {
7
- const chars = [];
8
- for (let i = 0; i < maxWords; i++) {
9
- const lo = ffi.readByte(ptr + offset + i * 2);
10
- const hi = ffi.readByte(ptr + offset + i * 2 + 1);
11
- const code = (hi << 8) | lo;
12
- if (code === 0)
13
- break;
14
- chars.push(String.fromCharCode(code));
15
- }
16
- return chars.join('');
17
- }
3
+ import { InvalidateRect } from '../../windows/user32.js';
4
+ import { NULL } from '../../ffi/ctype.js';
5
+ import { NMLINK } from '../../windows/structs.js';
6
+ import { nmCode } from '../nmhdr.js';
18
7
  const Link = forwardRef(function Link({ href, children, onClick, style }, ref) {
19
8
  const displayText = children != null ? String(children) : (href ?? '');
20
9
  const linkText = href ? `<A HREF="${href}">${displayText}</A>` : displayText;
@@ -25,15 +14,13 @@ const Link = forwardRef(function Link({ href, children, onClick, style }, ref) {
25
14
  const h = linkRef.current;
26
15
  if (!h)
27
16
  return;
28
- gui.InvalidateRect(h, null, true);
17
+ InvalidateRect(h, NULL, 1);
29
18
  }, [linkText]);
30
19
  return (_jsx("w", { type: "STATIC", ws: 268435456 /* gui.WindowStyle.VISIBLE */ | 33554432 /* gui.WindowStyle.CLIPCHILDREN */, style: { ...style, flexDirection: 'column', alignItems: 'stretch' }, ref: ref, onEvent: (e) => {
31
20
  if (e.msg === 78 /* gui.WmMsg.NOTIFY */) {
32
21
  const code = nmCode(e.lParam);
33
22
  if (code === -2 /* gui.SysLinkNotifyCode.CLICK */ || code === -4 /* gui.SysLinkNotifyCode.RETURN */) {
34
- // NMLINK: NMHDR(12/20) + LITEM{ mask,iLink,state,stateMask (16B) + szID[48] (96B) } → szUrl
35
- const url = readUtf16(e.lParam, NMHDR_SIZE + 112, 2048);
36
- onClickRef.current?.(url);
23
+ onClickRef.current?.(NMLINK.decode(e.lParam).item.szUrl);
37
24
  }
38
25
  }
39
26
  }, children: _jsx("w", { type: "SysLink", ws: 268435456 /* gui.WindowStyle.VISIBLE */ | 65536 /* gui.WindowStyle.TABSTOP */, text: linkText, style: { flexGrow: 1 }, ref: (h) => { linkRef.current = h; } }) }));